socket-netty 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynetty/__init__.py +137 -0
- pynetty/bootstrap/__init__.py +3 -0
- pynetty/bootstrap/bootstrap.py +162 -0
- pynetty/buffer/__init__.py +18 -0
- pynetty/buffer/allocator.py +113 -0
- pynetty/buffer/bytebuf.py +256 -0
- pynetty/channel/__init__.py +24 -0
- pynetty/channel/channel.py +130 -0
- pynetty/channel/channel_future.py +143 -0
- pynetty/channel/channel_option.py +59 -0
- pynetty/channel/channel_pipeline.py +173 -0
- pynetty/channel/datagram_channel.py +164 -0
- pynetty/channel/event_loop.py +157 -0
- pynetty/channel/flow_control.py +99 -0
- pynetty/channel/protocol_adapter.py +59 -0
- pynetty/exceptions.py +94 -0
- pynetty/handler/__init__.py +47 -0
- pynetty/handler/channel_handler.py +83 -0
- pynetty/handler/channel_handler_context.py +104 -0
- pynetty/handler/codec.py +152 -0
- pynetty/handler/protolib_codec.py +152 -0
- pynetty/handler/ssl_context.py +62 -0
- pynetty/handler/timeout.py +159 -0
- socket_netty-0.3.0.dist-info/METADATA +402 -0
- socket_netty-0.3.0.dist-info/RECORD +27 -0
- socket_netty-0.3.0.dist-info/WHEEL +5 -0
- socket_netty-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EventLoop / EventLoopGroup: equivalent to io.netty.channel.EventLoop and
|
|
3
|
+
EventLoopGroup.
|
|
4
|
+
|
|
5
|
+
In Netty (Java), an EventLoopGroup is a thread pool, each with its own
|
|
6
|
+
event loop, sharing out registered Channels (round-robin). In Python,
|
|
7
|
+
asyncio runs on a single thread per loop, so we replicate the model as
|
|
8
|
+
honestly as possible: each EventLoop in this module runs on its OWN
|
|
9
|
+
operating-system thread, with its own asyncio.EventLoop inside. An
|
|
10
|
+
EventLoopGroup shares Channels across several EventLoops that way —
|
|
11
|
+
real concurrency, not just cooperative.
|
|
12
|
+
|
|
13
|
+
For the simple single-thread case (99% of asyncio usage),
|
|
14
|
+
EventLoopGroup(num_threads=1) behaves the same as using
|
|
15
|
+
asyncio.get_event_loop() directly, but with Netty's API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import concurrent.futures
|
|
22
|
+
import itertools
|
|
23
|
+
import threading
|
|
24
|
+
from typing import Any, Callable, Coroutine, List, Optional, TypeVar
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EventLoop:
|
|
30
|
+
"""
|
|
31
|
+
Wraps an asyncio.AbstractEventLoop running on a dedicated thread.
|
|
32
|
+
Equivalent to io.netty.channel.EventLoop (a single-thread
|
|
33
|
+
execution "wheel").
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, name: str = "pynetty-eventloop") -> None:
|
|
37
|
+
self.name = name
|
|
38
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
39
|
+
self._thread: Optional[threading.Thread] = None
|
|
40
|
+
self._ready = threading.Event()
|
|
41
|
+
self._shutdown = False
|
|
42
|
+
|
|
43
|
+
def start(self) -> "EventLoop":
|
|
44
|
+
if self._thread is not None:
|
|
45
|
+
return self
|
|
46
|
+
self._thread = threading.Thread(target=self._run, name=self.name, daemon=True)
|
|
47
|
+
self._thread.start()
|
|
48
|
+
self._ready.wait()
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def _run(self) -> None:
|
|
52
|
+
self._loop = asyncio.new_event_loop()
|
|
53
|
+
asyncio.set_event_loop(self._loop)
|
|
54
|
+
self._ready.set()
|
|
55
|
+
try:
|
|
56
|
+
self._loop.run_forever()
|
|
57
|
+
finally:
|
|
58
|
+
self._loop.close()
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def loop(self) -> asyncio.AbstractEventLoop:
|
|
62
|
+
if self._loop is None:
|
|
63
|
+
raise RuntimeError(f"EventLoop {self.name!r} has not been started (call start())")
|
|
64
|
+
return self._loop
|
|
65
|
+
|
|
66
|
+
def in_event_loop(self) -> bool:
|
|
67
|
+
"""True if the calling code is already running inside this loop/thread."""
|
|
68
|
+
try:
|
|
69
|
+
return asyncio.get_running_loop() is self._loop
|
|
70
|
+
except RuntimeError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def submit(self, coro_factory: Callable[[], Coroutine[Any, Any, T]]) -> "concurrent.futures.Future[T]":
|
|
74
|
+
"""
|
|
75
|
+
Schedules a coroutine to run on this EventLoop from ANY thread
|
|
76
|
+
(including its own). Returns a concurrent.futures.Future that
|
|
77
|
+
you can block on with .result(), or wrap in asyncio with
|
|
78
|
+
asyncio.wrap_future().
|
|
79
|
+
"""
|
|
80
|
+
if self._loop is None:
|
|
81
|
+
raise RuntimeError(f"EventLoop {self.name!r} has not been started")
|
|
82
|
+
return asyncio.run_coroutine_threadsafe(coro_factory(), self._loop)
|
|
83
|
+
|
|
84
|
+
def call_soon(self, callback: Callable[..., None], *args: Any) -> None:
|
|
85
|
+
self.loop.call_soon_threadsafe(callback, *args)
|
|
86
|
+
|
|
87
|
+
def shutdown_gracefully(self, timeout: float = 5.0) -> None:
|
|
88
|
+
if self._loop is None or self._shutdown:
|
|
89
|
+
return
|
|
90
|
+
self._shutdown = True
|
|
91
|
+
|
|
92
|
+
async def _stop_all_tasks() -> None:
|
|
93
|
+
tasks = [t for t in asyncio.all_tasks(self._loop) if t is not asyncio.current_task()]
|
|
94
|
+
for t in tasks:
|
|
95
|
+
t.cancel()
|
|
96
|
+
if tasks:
|
|
97
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
98
|
+
|
|
99
|
+
fut = asyncio.run_coroutine_threadsafe(_stop_all_tasks(), self._loop)
|
|
100
|
+
try:
|
|
101
|
+
fut.result(timeout=timeout)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
105
|
+
if self._thread is not None:
|
|
106
|
+
self._thread.join(timeout=timeout)
|
|
107
|
+
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
return f"EventLoop(name={self.name!r}, running={self._loop is not None})"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class EventLoopGroup:
|
|
113
|
+
"""
|
|
114
|
+
Pool of EventLoop instances. Equivalent to Netty's NioEventLoopGroup.
|
|
115
|
+
Shares out EventLoops round-robin via next_loop().
|
|
116
|
+
|
|
117
|
+
Typical usage (as in Netty):
|
|
118
|
+
boss_group = EventLoopGroup(1) # accepts connections
|
|
119
|
+
worker_group = EventLoopGroup(4) # processes channel I/O
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, num_threads: int = 1, name_prefix: str = "pynetty-worker") -> None:
|
|
123
|
+
if num_threads < 1:
|
|
124
|
+
raise ValueError("num_threads must be >= 1")
|
|
125
|
+
self._loops: List[EventLoop] = [
|
|
126
|
+
EventLoop(name=f"{name_prefix}-{i}") for i in range(num_threads)
|
|
127
|
+
]
|
|
128
|
+
self._cycle = itertools.cycle(self._loops)
|
|
129
|
+
self._started = False
|
|
130
|
+
|
|
131
|
+
def start(self) -> "EventLoopGroup":
|
|
132
|
+
if self._started:
|
|
133
|
+
return self
|
|
134
|
+
for loop in self._loops:
|
|
135
|
+
loop.start()
|
|
136
|
+
self._started = True
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def next_loop(self) -> EventLoop:
|
|
140
|
+
"""Returns the next EventLoop round-robin (equivalent to group.next() in Netty)."""
|
|
141
|
+
if not self._started:
|
|
142
|
+
self.start()
|
|
143
|
+
return next(self._cycle)
|
|
144
|
+
|
|
145
|
+
def all_loops(self) -> List[EventLoop]:
|
|
146
|
+
return list(self._loops)
|
|
147
|
+
|
|
148
|
+
def shutdown_gracefully(self, timeout: float = 5.0) -> None:
|
|
149
|
+
for loop in self._loops:
|
|
150
|
+
loop.shutdown_gracefully(timeout=timeout)
|
|
151
|
+
self._started = False
|
|
152
|
+
|
|
153
|
+
def __len__(self) -> int:
|
|
154
|
+
return len(self._loops)
|
|
155
|
+
|
|
156
|
+
def __repr__(self) -> str:
|
|
157
|
+
return f"EventLoopGroup(threads={len(self._loops)})"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backpressure and Concurrency: equivalent to
|
|
3
|
+
io.netty.channel.WriteBufferWaterMark and Netty's one-thread-per-loop
|
|
4
|
+
model.
|
|
5
|
+
|
|
6
|
+
- WriteBufferWaterMark: high/low thresholds to pause/resume writes
|
|
7
|
+
when the socket's outbound buffer fills up (asyncio already exposes
|
|
8
|
+
this via transport.set_write_buffer_limits +
|
|
9
|
+
pause_writing()/resume_writing() on the Protocol; here we integrate
|
|
10
|
+
it with the pipeline via channel_writability_changed).
|
|
11
|
+
- ChannelExecutor: guarantees that a given Channel's code always runs
|
|
12
|
+
serialized (one message at a time), equivalent to Netty "pinning"
|
|
13
|
+
each Channel to a single EventLoop/thread.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WriteBufferWaterMark:
|
|
23
|
+
"""
|
|
24
|
+
Write buffer thresholds. When the pending buffer exceeds `high`,
|
|
25
|
+
the channel is marked non-writable (backpressure); once it drops
|
|
26
|
+
below `low`, it's marked writable again.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, low: int = 32 * 1024, high: int = 64 * 1024) -> None:
|
|
30
|
+
if low > high:
|
|
31
|
+
raise ValueError("low cannot be greater than high")
|
|
32
|
+
self.low = low
|
|
33
|
+
self.high = high
|
|
34
|
+
|
|
35
|
+
def apply(self, transport: asyncio.Transport) -> None:
|
|
36
|
+
transport.set_write_buffer_limits(high=self.high, low=self.low)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ChannelExecutor:
|
|
40
|
+
"""
|
|
41
|
+
Serializes a Channel's message processing using an internal queue +
|
|
42
|
+
a single worker task — equivalent to Netty's guarantee that all of
|
|
43
|
+
a Channel's events run on the same thread, one at a time, without
|
|
44
|
+
racing against each other.
|
|
45
|
+
|
|
46
|
+
Usage: instead of creating a task per channel_read, work is queued
|
|
47
|
+
here, guaranteeing order and avoiding concurrent re-entrancy over
|
|
48
|
+
the same handler/state.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self._queue: asyncio.Queue = asyncio.Queue()
|
|
53
|
+
self._worker: asyncio.Task | None = None
|
|
54
|
+
self._running = False
|
|
55
|
+
|
|
56
|
+
def start(self) -> None:
|
|
57
|
+
if self._worker is not None:
|
|
58
|
+
return
|
|
59
|
+
self._running = True
|
|
60
|
+
self._worker = asyncio.get_event_loop().create_task(self._run())
|
|
61
|
+
|
|
62
|
+
async def submit(self, coro) -> None:
|
|
63
|
+
await self._queue.put(coro)
|
|
64
|
+
|
|
65
|
+
async def _run(self) -> None:
|
|
66
|
+
while True:
|
|
67
|
+
coro = await self._queue.get()
|
|
68
|
+
if coro is None:
|
|
69
|
+
break
|
|
70
|
+
try:
|
|
71
|
+
await coro
|
|
72
|
+
except Exception:
|
|
73
|
+
# The pipeline itself already handles exceptions via
|
|
74
|
+
# exception_caught; this is a last-resort safety net.
|
|
75
|
+
pass
|
|
76
|
+
finally:
|
|
77
|
+
self._queue.task_done()
|
|
78
|
+
|
|
79
|
+
async def stop(self) -> None:
|
|
80
|
+
self._running = False
|
|
81
|
+
await self._queue.put(None)
|
|
82
|
+
if self._worker is not None:
|
|
83
|
+
await self._worker
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FlowControlInboundHandler:
|
|
87
|
+
"""
|
|
88
|
+
Optional mixin: adds channel_writability_changed handling to an
|
|
89
|
+
existing ChannelInboundHandler. Not a standalone handler — the
|
|
90
|
+
user is expected to combine it or check ctx.channel.is_writable()
|
|
91
|
+
before continuing to write aggressively (e.g. when streaming many
|
|
92
|
+
packets back to back).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
async def wait_writable(channel: Any, poll_interval: float = 0.01) -> None:
|
|
97
|
+
"""Cooperatively waits until the channel becomes writable again."""
|
|
98
|
+
while not channel.is_writable():
|
|
99
|
+
await asyncio.sleep(poll_interval)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_ChannelProtocol: bridge between asyncio.Protocol and our Channel.
|
|
3
|
+
|
|
4
|
+
asyncio calls these callbacks synchronously; here we convert them into
|
|
5
|
+
coroutines scheduled on the loop so we can fire the pipeline (which is
|
|
6
|
+
async end-to-end) without blocking.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
|
|
14
|
+
from pynetty.channel.channel import Channel
|
|
15
|
+
from pynetty.channel.flow_control import WriteBufferWaterMark
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _ChannelProtocol(asyncio.Protocol):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
channel_factory: Callable[[], Channel],
|
|
22
|
+
water_mark: Optional[WriteBufferWaterMark] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.channel: Channel = channel_factory()
|
|
25
|
+
self._loop = asyncio.get_event_loop()
|
|
26
|
+
self._water_mark = water_mark
|
|
27
|
+
|
|
28
|
+
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
|
29
|
+
self.channel._attach_transport(transport)
|
|
30
|
+
if self._water_mark is not None:
|
|
31
|
+
self._water_mark.apply(transport) # type: ignore[arg-type]
|
|
32
|
+
self._loop.create_task(self._safe(self.channel._mark_active()))
|
|
33
|
+
|
|
34
|
+
def data_received(self, data: bytes) -> None:
|
|
35
|
+
self._loop.create_task(self._safe(self.channel._fire_read(data)))
|
|
36
|
+
|
|
37
|
+
def pause_writing(self) -> None:
|
|
38
|
+
self.channel._set_writable(False)
|
|
39
|
+
|
|
40
|
+
def resume_writing(self) -> None:
|
|
41
|
+
self.channel._set_writable(True)
|
|
42
|
+
|
|
43
|
+
def connection_lost(self, exc: Optional[Exception]) -> None:
|
|
44
|
+
if exc is not None:
|
|
45
|
+
self._loop.create_task(self._safe(self.channel._fire_exception(exc)))
|
|
46
|
+
self._loop.create_task(self._safe(self.channel._mark_inactive()))
|
|
47
|
+
|
|
48
|
+
async def _safe(self, coro) -> None:
|
|
49
|
+
try:
|
|
50
|
+
await coro
|
|
51
|
+
except Exception as exc: # noqa: BLE001 - last resort: the task must not die
|
|
52
|
+
try:
|
|
53
|
+
await self.channel._fire_exception(exc)
|
|
54
|
+
except Exception:
|
|
55
|
+
# If even the exception handling fails, there's nothing
|
|
56
|
+
# left to do but swallow it silently here; the user
|
|
57
|
+
# should have a robust exception_caught.
|
|
58
|
+
pass
|
|
59
|
+
|
pynetty/exceptions.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions mirroring Netty's own hierarchy under io.netty.*
|
|
3
|
+
|
|
4
|
+
Every exception here carries a `netty_path` class attribute matching the
|
|
5
|
+
fully-qualified Java class it corresponds to. __str__ prefixes messages
|
|
6
|
+
with that path, so a caught exception prints exactly like it would coming
|
|
7
|
+
out of a real Netty stack, e.g.:
|
|
8
|
+
|
|
9
|
+
io.netty.handler.codec.DecoderException: Failed to decode packet
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NettyException(Exception):
|
|
16
|
+
"""Base class for all pynetty exceptions; mirrors io.netty.channel.ChannelException lineage."""
|
|
17
|
+
|
|
18
|
+
netty_path: str = "io.netty.channel.ChannelException"
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
msg = super().__str__()
|
|
22
|
+
return f"{self.netty_path}: {msg}" if msg else self.netty_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ----------------------------------------------------------------------
|
|
26
|
+
# io.netty.channel.*
|
|
27
|
+
# ----------------------------------------------------------------------
|
|
28
|
+
class ChannelException(NettyException):
|
|
29
|
+
netty_path = "io.netty.channel.ChannelException"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DuplicateHandlerNameError(NettyException):
|
|
33
|
+
netty_path = "io.netty.channel.ChannelPipelineException"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------
|
|
37
|
+
# io.netty.handler.codec.*
|
|
38
|
+
# ----------------------------------------------------------------------
|
|
39
|
+
class CodecException(NettyException):
|
|
40
|
+
netty_path = "io.netty.handler.codec.CodecException"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DecoderException(CodecException):
|
|
44
|
+
netty_path = "io.netty.handler.codec.DecoderException"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EncoderException(CodecException):
|
|
48
|
+
netty_path = "io.netty.handler.codec.EncoderException"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CorruptedFrameException(DecoderException):
|
|
52
|
+
netty_path = "io.netty.handler.codec.CorruptedFrameException"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TooLongFrameException(DecoderException):
|
|
56
|
+
netty_path = "io.netty.handler.codec.TooLongFrameException"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ----------------------------------------------------------------------
|
|
60
|
+
# io.netty.handler.timeout.*
|
|
61
|
+
# ----------------------------------------------------------------------
|
|
62
|
+
class TimeoutException(NettyException):
|
|
63
|
+
netty_path = "io.netty.handler.timeout.TimeoutException"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ReadTimeoutError(TimeoutException):
|
|
67
|
+
netty_path = "io.netty.handler.timeout.ReadTimeoutException"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class WriteTimeoutError(TimeoutException):
|
|
71
|
+
netty_path = "io.netty.handler.timeout.WriteTimeoutException"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ----------------------------------------------------------------------
|
|
75
|
+
# io.netty.buffer.*
|
|
76
|
+
# ----------------------------------------------------------------------
|
|
77
|
+
class IndexOutOfBoundsError(NettyException):
|
|
78
|
+
netty_path = "io.netty.buffer.IndexOutOfBoundsException"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"NettyException",
|
|
83
|
+
"ChannelException",
|
|
84
|
+
"DuplicateHandlerNameError",
|
|
85
|
+
"CodecException",
|
|
86
|
+
"DecoderException",
|
|
87
|
+
"EncoderException",
|
|
88
|
+
"CorruptedFrameException",
|
|
89
|
+
"TooLongFrameException",
|
|
90
|
+
"TimeoutException",
|
|
91
|
+
"ReadTimeoutError",
|
|
92
|
+
"WriteTimeoutError",
|
|
93
|
+
"IndexOutOfBoundsError",
|
|
94
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pynetty.handler.channel_handler import (
|
|
2
|
+
ChannelHandler,
|
|
3
|
+
ChannelInboundHandler,
|
|
4
|
+
ChannelOutboundHandler,
|
|
5
|
+
ChannelInboundHandlerAdapter,
|
|
6
|
+
ChannelOutboundHandlerAdapter,
|
|
7
|
+
SimpleChannelInboundHandler,
|
|
8
|
+
)
|
|
9
|
+
from pynetty.handler.channel_handler_context import ChannelHandlerContext
|
|
10
|
+
from pynetty.handler.codec import (
|
|
11
|
+
LengthFieldBasedFrameDecoder,
|
|
12
|
+
LengthFieldPrepender,
|
|
13
|
+
ByteToMessageCodec,
|
|
14
|
+
)
|
|
15
|
+
from pynetty.handler.protolib_codec import ProtolibCodec
|
|
16
|
+
from pynetty.handler.ssl_context import SslContextBuilder
|
|
17
|
+
from pynetty.handler.timeout import (
|
|
18
|
+
IdleState,
|
|
19
|
+
IdleStateEvent,
|
|
20
|
+
IdleStateHandler,
|
|
21
|
+
ReadTimeoutHandler,
|
|
22
|
+
ReadTimeoutError,
|
|
23
|
+
WriteTimeoutHandler,
|
|
24
|
+
WriteTimeoutError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ChannelHandler",
|
|
29
|
+
"ChannelInboundHandler",
|
|
30
|
+
"ChannelOutboundHandler",
|
|
31
|
+
"ChannelInboundHandlerAdapter",
|
|
32
|
+
"ChannelOutboundHandlerAdapter",
|
|
33
|
+
"SimpleChannelInboundHandler",
|
|
34
|
+
"ChannelHandlerContext",
|
|
35
|
+
"LengthFieldBasedFrameDecoder",
|
|
36
|
+
"LengthFieldPrepender",
|
|
37
|
+
"ByteToMessageCodec",
|
|
38
|
+
"ProtolibCodec",
|
|
39
|
+
"SslContextBuilder",
|
|
40
|
+
"IdleState",
|
|
41
|
+
"IdleStateEvent",
|
|
42
|
+
"IdleStateHandler",
|
|
43
|
+
"ReadTimeoutHandler",
|
|
44
|
+
"ReadTimeoutError",
|
|
45
|
+
"WriteTimeoutHandler",
|
|
46
|
+
"WriteTimeoutError",
|
|
47
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChannelHandler: basic unit of event processing in the pipeline.
|
|
3
|
+
|
|
4
|
+
Follows Netty's model: handlers receive events (inbound) or intercept
|
|
5
|
+
write operations (outbound) through a ChannelHandlerContext, and decide
|
|
6
|
+
whether to propagate them to the next handler in the chain.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pynetty.handler.channel_handler_context import ChannelHandlerContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChannelHandler:
|
|
18
|
+
"""Base class that all handlers inherit from."""
|
|
19
|
+
|
|
20
|
+
async def handler_added(self, ctx: "ChannelHandlerContext") -> None:
|
|
21
|
+
"""Called when the handler is added to the pipeline."""
|
|
22
|
+
|
|
23
|
+
async def handler_removed(self, ctx: "ChannelHandlerContext") -> None:
|
|
24
|
+
"""Called when the handler is removed from the pipeline."""
|
|
25
|
+
|
|
26
|
+
async def exception_caught(self, ctx: "ChannelHandlerContext", exc: Exception) -> None:
|
|
27
|
+
"""Default exception handling: propagates to the next handler."""
|
|
28
|
+
await ctx.fire_exception_caught(exc)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ChannelInboundHandler(ChannelHandler):
|
|
32
|
+
"""Handler for inbound events: connection, data read, close, etc."""
|
|
33
|
+
|
|
34
|
+
async def channel_registered(self, ctx: "ChannelHandlerContext") -> None:
|
|
35
|
+
await ctx.fire_channel_registered()
|
|
36
|
+
|
|
37
|
+
async def channel_active(self, ctx: "ChannelHandlerContext") -> None:
|
|
38
|
+
await ctx.fire_channel_active()
|
|
39
|
+
|
|
40
|
+
async def channel_inactive(self, ctx: "ChannelHandlerContext") -> None:
|
|
41
|
+
await ctx.fire_channel_inactive()
|
|
42
|
+
|
|
43
|
+
async def channel_read(self, ctx: "ChannelHandlerContext", msg: Any) -> None:
|
|
44
|
+
await ctx.fire_channel_read(msg)
|
|
45
|
+
|
|
46
|
+
async def channel_read_complete(self, ctx: "ChannelHandlerContext") -> None:
|
|
47
|
+
await ctx.fire_channel_read_complete()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ChannelOutboundHandler(ChannelHandler):
|
|
51
|
+
"""Handler for outbound operations: write, close, etc."""
|
|
52
|
+
|
|
53
|
+
async def write(self, ctx: "ChannelHandlerContext", msg: Any) -> None:
|
|
54
|
+
await ctx.write(msg)
|
|
55
|
+
|
|
56
|
+
async def close(self, ctx: "ChannelHandlerContext") -> None:
|
|
57
|
+
await ctx.close()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChannelInboundHandlerAdapter(ChannelInboundHandler):
|
|
61
|
+
"""Alias for compatibility with Netty's naming (ChannelInboundHandlerAdapter)."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ChannelOutboundHandlerAdapter(ChannelOutboundHandler):
|
|
65
|
+
"""Alias for compatibility with Netty's naming (ChannelOutboundHandlerAdapter)."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SimpleChannelInboundHandler(ChannelInboundHandler):
|
|
69
|
+
"""
|
|
70
|
+
Equivalent to Netty's SimpleChannelInboundHandler: automatically
|
|
71
|
+
releases/discards the message after processing it. Python has no
|
|
72
|
+
manual refcounting like Netty's (pooled ByteBuf), so this mainly
|
|
73
|
+
serves as a semantic extension point.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
async def channel_read(self, ctx: "ChannelHandlerContext", msg: Any) -> None:
|
|
77
|
+
try:
|
|
78
|
+
await self.channel_read0(ctx, msg)
|
|
79
|
+
finally:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
async def channel_read0(self, ctx: "ChannelHandlerContext", msg: Any) -> None:
|
|
83
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChannelHandlerContext: represents the association between a
|
|
3
|
+
ChannelHandler and its position within the ChannelPipeline. Lets you
|
|
4
|
+
propagate events forward (inbound) or backward (outbound) in the chain.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pynetty.channel.channel import Channel
|
|
13
|
+
from pynetty.handler.channel_handler import ChannelHandler
|
|
14
|
+
from pynetty.channel.channel_pipeline import ChannelPipeline
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChannelHandlerContext:
|
|
18
|
+
__slots__ = ("name", "handler", "pipeline", "_next", "_prev")
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str, handler: "ChannelHandler", pipeline: "ChannelPipeline") -> None:
|
|
21
|
+
self.name = name
|
|
22
|
+
self.handler = handler
|
|
23
|
+
self.pipeline = pipeline
|
|
24
|
+
self._next: "ChannelHandlerContext | None" = None
|
|
25
|
+
self._prev: "ChannelHandlerContext | None" = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def channel(self) -> "Channel":
|
|
29
|
+
return self.pipeline.channel
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Inbound propagation (toward the next handler in the chain)
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
async def fire_channel_registered(self) -> None:
|
|
35
|
+
nxt = self._find_next_inbound()
|
|
36
|
+
if nxt is not None:
|
|
37
|
+
await nxt.handler.channel_registered(nxt)
|
|
38
|
+
|
|
39
|
+
async def fire_channel_active(self) -> None:
|
|
40
|
+
nxt = self._find_next_inbound()
|
|
41
|
+
if nxt is not None:
|
|
42
|
+
await nxt.handler.channel_active(nxt)
|
|
43
|
+
|
|
44
|
+
async def fire_channel_inactive(self) -> None:
|
|
45
|
+
nxt = self._find_next_inbound()
|
|
46
|
+
if nxt is not None:
|
|
47
|
+
await nxt.handler.channel_inactive(nxt)
|
|
48
|
+
|
|
49
|
+
async def fire_channel_read(self, msg: Any) -> None:
|
|
50
|
+
nxt = self._find_next_inbound()
|
|
51
|
+
if nxt is not None:
|
|
52
|
+
await nxt.handler.channel_read(nxt, msg)
|
|
53
|
+
|
|
54
|
+
async def fire_channel_read_complete(self) -> None:
|
|
55
|
+
nxt = self._find_next_inbound()
|
|
56
|
+
if nxt is not None:
|
|
57
|
+
await nxt.handler.channel_read_complete(nxt)
|
|
58
|
+
|
|
59
|
+
async def fire_exception_caught(self, exc: Exception) -> None:
|
|
60
|
+
nxt = self._find_next_inbound()
|
|
61
|
+
if nxt is not None:
|
|
62
|
+
await nxt.handler.exception_caught(nxt, exc)
|
|
63
|
+
else:
|
|
64
|
+
# No one else is left in the chain: don't swallow it silently.
|
|
65
|
+
raise exc
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Outbound propagation (backward, toward the socket)
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
async def write(self, msg: Any) -> None:
|
|
71
|
+
prev = self._find_prev_outbound()
|
|
72
|
+
if prev is not None:
|
|
73
|
+
await prev.handler.write(prev, msg)
|
|
74
|
+
else:
|
|
75
|
+
await self.channel._do_write(msg)
|
|
76
|
+
|
|
77
|
+
async def close(self) -> None:
|
|
78
|
+
prev = self._find_prev_outbound()
|
|
79
|
+
if prev is not None:
|
|
80
|
+
await prev.handler.close(prev)
|
|
81
|
+
else:
|
|
82
|
+
await self.channel._do_close()
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Lookup of relevant handlers by type
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
def _find_next_inbound(self) -> "ChannelHandlerContext | None":
|
|
88
|
+
from pynetty.handler.channel_handler import ChannelInboundHandler
|
|
89
|
+
|
|
90
|
+
node = self._next
|
|
91
|
+
while node is not None and not isinstance(node.handler, ChannelInboundHandler):
|
|
92
|
+
node = node._next
|
|
93
|
+
return node
|
|
94
|
+
|
|
95
|
+
def _find_prev_outbound(self) -> "ChannelHandlerContext | None":
|
|
96
|
+
from pynetty.handler.channel_handler import ChannelOutboundHandler
|
|
97
|
+
|
|
98
|
+
node = self._prev
|
|
99
|
+
while node is not None and not isinstance(node.handler, ChannelOutboundHandler):
|
|
100
|
+
node = node._prev
|
|
101
|
+
return node
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
return f"ChannelHandlerContext(name={self.name!r}, handler={self.handler.__class__.__name__})"
|