Add some internal documenation
This commit is contained in:
parent
44dc2d06c6
commit
464dc23ff0
|
@ -1,6 +1,6 @@
|
|||
##########################################################
|
||||
# This RNS example demonstrates how to set up a link to #
|
||||
# a destination, and pass structuredmessages over it #
|
||||
# a destination, and pass structured messages over it #
|
||||
# using a channel. #
|
||||
##########################################################
|
||||
|
||||
|
@ -46,7 +46,7 @@ class StringMessage(RNS.MessageBase):
|
|||
# message arrives over the channel.
|
||||
#
|
||||
# MSGTYPE must be unique across all message types we
|
||||
# register with the channel. MSGTYPEs >= 0xff00 are
|
||||
# register with the channel. MSGTYPEs >= 0xf000 are
|
||||
# reserved for the system.
|
||||
MSGTYPE = 0x0101
|
||||
|
||||
|
@ -159,17 +159,36 @@ def client_disconnected(link):
|
|||
RNS.log("Client disconnected")
|
||||
|
||||
def server_message_received(message):
|
||||
"""
|
||||
A message handler
|
||||
@param message: An instance of a subclass of MessageBase
|
||||
@return: True if message was handled
|
||||
"""
|
||||
global latest_client_link
|
||||
|
||||
# When a message is received over any active link,
|
||||
# the replies will all be directed to the last client
|
||||
# that connected.
|
||||
|
||||
# In a message handler, any deserializable message
|
||||
# that arrives over the link's channel will be passed
|
||||
# to all message handlers, unless a preceding handler indicates it
|
||||
# has handled the message.
|
||||
#
|
||||
#
|
||||
if isinstance(message, StringMessage):
|
||||
RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")")
|
||||
|
||||
reply_message = StringMessage("I received \""+message.data+"\" over the link")
|
||||
latest_client_link.get_channel().send(reply_message)
|
||||
|
||||
# Incoming messages are sent to each message
|
||||
# handler added to the channel, in the order they
|
||||
# were added.
|
||||
# If any message handler returns True, the message
|
||||
# is considered handled and any subsequent
|
||||
# handlers are skipped.
|
||||
return True
|
||||
|
||||
|
||||
##########################################################
|
||||
#### Client Part #########################################
|
||||
|
|
109
RNS/Channel.py
109
RNS/Channel.py
|
@ -14,6 +14,13 @@ TPacket = TypeVar("TPacket")
|
|||
|
||||
|
||||
class ChannelOutletBase(ABC, Generic[TPacket]):
|
||||
"""
|
||||
An abstract transport layer interface used by Channel.
|
||||
|
||||
DEPRECATED: This was created for testing; eventually
|
||||
Channel will use Link or a LinkBase interface
|
||||
directly.
|
||||
"""
|
||||
@abstractmethod
|
||||
def send(self, raw: bytes) -> TPacket:
|
||||
raise NotImplemented()
|
||||
|
@ -64,6 +71,9 @@ class ChannelOutletBase(ABC, Generic[TPacket]):
|
|||
|
||||
|
||||
class CEType(enum.IntEnum):
|
||||
"""
|
||||
ChannelException type codes
|
||||
"""
|
||||
ME_NO_MSG_TYPE = 0
|
||||
ME_INVALID_MSG_TYPE = 1
|
||||
ME_NOT_REGISTERED = 2
|
||||
|
@ -73,12 +83,18 @@ class CEType(enum.IntEnum):
|
|||
|
||||
|
||||
class ChannelException(Exception):
|
||||
"""
|
||||
An exception thrown by Channel, with a type code.
|
||||
"""
|
||||
def __init__(self, ce_type: CEType, *args):
|
||||
super().__init__(args)
|
||||
self.type = ce_type
|
||||
|
||||
|
||||
class MessageState(enum.IntEnum):
|
||||
"""
|
||||
Set of possible states for a Message
|
||||
"""
|
||||
MSGSTATE_NEW = 0
|
||||
MSGSTATE_SENT = 1
|
||||
MSGSTATE_DELIVERED = 2
|
||||
|
@ -86,14 +102,29 @@ class MessageState(enum.IntEnum):
|
|||
|
||||
|
||||
class MessageBase(abc.ABC):
|
||||
"""
|
||||
Base type for any messages sent or received on a Channel.
|
||||
Subclasses must define the two abstract methods as well as
|
||||
the MSGTYPE class variable.
|
||||
"""
|
||||
# MSGTYPE must be unique within all classes sent over a
|
||||
# channel. Additionally, MSGTYPE > 0xf000 are reserved.
|
||||
MSGTYPE = None
|
||||
|
||||
@abstractmethod
|
||||
def pack(self) -> bytes:
|
||||
"""
|
||||
Create and return the binary representation of the message
|
||||
@return: binary representation of message
|
||||
"""
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def unpack(self, raw):
|
||||
"""
|
||||
Populate message from binary representation
|
||||
@param raw: binary representation
|
||||
"""
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
|
@ -101,6 +132,10 @@ MessageCallbackType = NewType("MessageCallbackType", Callable[[MessageBase], boo
|
|||
|
||||
|
||||
class Envelope:
|
||||
"""
|
||||
Internal wrapper used to transport messages over a channel and
|
||||
track its state within the channel framework.
|
||||
"""
|
||||
def unpack(self, message_factories: dict[int, Type]) -> MessageBase:
|
||||
msgtype, self.sequence, length = struct.unpack(">HHH", self.raw[:6])
|
||||
raw = self.raw[6:]
|
||||
|
@ -131,6 +166,12 @@ class Envelope:
|
|||
|
||||
|
||||
class Channel(contextlib.AbstractContextManager):
|
||||
"""
|
||||
Channel provides reliable delivery of messages over
|
||||
a link. Channel is not meant to be instantiated
|
||||
directly, but rather obtained from a Link using the
|
||||
get_channel() function.
|
||||
"""
|
||||
def __init__(self, outlet: ChannelOutletBase):
|
||||
self._outlet = outlet
|
||||
self._lock = threading.RLock()
|
||||
|
@ -146,10 +187,14 @@ class Channel(contextlib.AbstractContextManager):
|
|||
|
||||
def __exit__(self, __exc_type: Type[BaseException] | None, __exc_value: BaseException | None,
|
||||
__traceback: TracebackType | None) -> bool | None:
|
||||
self.shutdown()
|
||||
self._shutdown()
|
||||
return False
|
||||
|
||||
def register_message_type(self, message_class: Type[MessageBase], *, is_system_type: bool = False):
|
||||
"""
|
||||
Register a message class for reception over a channel.
|
||||
@param message_class: Class to register. Must extend MessageBase.
|
||||
"""
|
||||
with self._lock:
|
||||
if not issubclass(message_class, MessageBase):
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
|
@ -157,7 +202,7 @@ class Channel(contextlib.AbstractContextManager):
|
|||
if message_class.MSGTYPE is None:
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
f"{message_class} has invalid MSGTYPE class attribute.")
|
||||
if message_class.MSGTYPE >= 0xff00 and not is_system_type:
|
||||
if message_class.MSGTYPE >= 0xf000 and not is_system_type:
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
f"{message_class} has system-reserved message type.")
|
||||
try:
|
||||
|
@ -169,20 +214,34 @@ class Channel(contextlib.AbstractContextManager):
|
|||
self._message_factories[message_class.MSGTYPE] = message_class
|
||||
|
||||
def add_message_handler(self, callback: MessageCallbackType):
|
||||
"""
|
||||
Add a handler for incoming messages. A handler
|
||||
has the signature (message: MessageBase) -> bool.
|
||||
Handlers are processed in the order they are
|
||||
added. If any handler returns True, processing
|
||||
of the message stops; handlers after the
|
||||
returning handler will not be called.
|
||||
@param callback: Function to call
|
||||
@return:
|
||||
"""
|
||||
with self._lock:
|
||||
if callback not in self._message_callbacks:
|
||||
self._message_callbacks.append(callback)
|
||||
|
||||
def remove_message_handler(self, callback: MessageCallbackType):
|
||||
"""
|
||||
Remove a handler
|
||||
@param callback: handler to remove
|
||||
"""
|
||||
with self._lock:
|
||||
self._message_callbacks.remove(callback)
|
||||
|
||||
def shutdown(self):
|
||||
def _shutdown(self):
|
||||
with self._lock:
|
||||
self._message_callbacks.clear()
|
||||
self.clear_rings()
|
||||
self._clear_rings()
|
||||
|
||||
def clear_rings(self):
|
||||
def _clear_rings(self):
|
||||
with self._lock:
|
||||
for envelope in self._tx_ring:
|
||||
if envelope.packet is not None:
|
||||
|
@ -191,14 +250,15 @@ class Channel(contextlib.AbstractContextManager):
|
|||
self._tx_ring.clear()
|
||||
self._rx_ring.clear()
|
||||
|
||||
def emplace_envelope(self, envelope: Envelope, ring: collections.deque[Envelope]) -> bool:
|
||||
def _emplace_envelope(self, envelope: Envelope, ring: collections.deque[Envelope]) -> bool:
|
||||
with self._lock:
|
||||
i = 0
|
||||
for env in ring:
|
||||
if env.sequence < envelope.sequence:
|
||||
for existing in ring:
|
||||
if existing.sequence > envelope.sequence \
|
||||
and not existing.sequence // 2 > envelope.sequence: # account for overflow
|
||||
ring.insert(i, envelope)
|
||||
return True
|
||||
if env.sequence == envelope.sequence:
|
||||
if existing.sequence == envelope.sequence:
|
||||
RNS.log(f"Envelope: Emplacement of duplicate envelope sequence.", RNS.LOG_EXTREME)
|
||||
return False
|
||||
i += 1
|
||||
|
@ -206,7 +266,7 @@ class Channel(contextlib.AbstractContextManager):
|
|||
ring.append(envelope)
|
||||
return True
|
||||
|
||||
def prune_rx_ring(self):
|
||||
def _prune_rx_ring(self):
|
||||
with self._lock:
|
||||
# Implementation for fixed window = 1
|
||||
stale = list(sorted(self._rx_ring, key=lambda env: env.sequence, reverse=True))[1:]
|
||||
|
@ -225,13 +285,13 @@ class Channel(contextlib.AbstractContextManager):
|
|||
except Exception as ex:
|
||||
RNS.log(f"Channel: Error running message callback: {ex}", RNS.LOG_ERROR)
|
||||
|
||||
def receive(self, raw: bytes):
|
||||
def _receive(self, raw: bytes):
|
||||
try:
|
||||
envelope = Envelope(outlet=self._outlet, raw=raw)
|
||||
with self._lock:
|
||||
message = envelope.unpack(self._message_factories)
|
||||
is_new = self.emplace_envelope(envelope, self._rx_ring)
|
||||
self.prune_rx_ring()
|
||||
is_new = self._emplace_envelope(envelope, self._rx_ring)
|
||||
self._prune_rx_ring()
|
||||
if not is_new:
|
||||
RNS.log("Channel: Duplicate message received", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
@ -241,6 +301,10 @@ class Channel(contextlib.AbstractContextManager):
|
|||
RNS.log(f"Channel: Error receiving data: {ex}")
|
||||
|
||||
def is_ready_to_send(self) -> bool:
|
||||
"""
|
||||
Check if Channel is ready to send.
|
||||
@return: True if ready
|
||||
"""
|
||||
if not self._outlet.is_usable:
|
||||
RNS.log("Channel: Link is not usable.", RNS.LOG_EXTREME)
|
||||
return False
|
||||
|
@ -273,7 +337,7 @@ class Channel(contextlib.AbstractContextManager):
|
|||
def retry_envelope(envelope: Envelope) -> bool:
|
||||
if envelope.tries >= self._max_tries:
|
||||
RNS.log("Channel: Retry count exceeded, tearing down Link.", RNS.LOG_ERROR)
|
||||
self.shutdown() # start on separate thread?
|
||||
self._shutdown() # start on separate thread?
|
||||
self._outlet.timed_out()
|
||||
return True
|
||||
envelope.tries += 1
|
||||
|
@ -283,13 +347,18 @@ class Channel(contextlib.AbstractContextManager):
|
|||
self._packet_tx_op(packet, retry_envelope)
|
||||
|
||||
def send(self, message: MessageBase) -> Envelope:
|
||||
"""
|
||||
Send a message. If a message send is attempted and
|
||||
Channel is not ready, an exception is thrown.
|
||||
@param message: an instance of a MessageBase subclass to send on the Channel
|
||||
"""
|
||||
envelope: Envelope | None = None
|
||||
with self._lock:
|
||||
if not self.is_ready_to_send():
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
|
||||
envelope = Envelope(self._outlet, message=message, sequence=self._next_sequence)
|
||||
self._next_sequence = (self._next_sequence + 1) % 0x10000
|
||||
self.emplace_envelope(envelope, self._tx_ring)
|
||||
self._emplace_envelope(envelope, self._tx_ring)
|
||||
if envelope is None:
|
||||
raise BlockingIOError()
|
||||
|
||||
|
@ -304,10 +373,20 @@ class Channel(contextlib.AbstractContextManager):
|
|||
|
||||
@property
|
||||
def MDU(self):
|
||||
"""
|
||||
Maximum Data Unit: the number of bytes available
|
||||
for a message to consume in a single send.
|
||||
@return: number of bytes available
|
||||
"""
|
||||
return self._outlet.mdu - 6 # sizeof(msgtype) + sizeof(length) + sizeof(sequence)
|
||||
|
||||
|
||||
class LinkChannelOutlet(ChannelOutletBase):
|
||||
"""
|
||||
An implementation of ChannelOutletBase for RNS.Link.
|
||||
Allows Channel to send packets over an RNS Link with
|
||||
Packets.
|
||||
"""
|
||||
def __init__(self, link: RNS.Link):
|
||||
self.link = link
|
||||
|
||||
|
|
|
@ -464,7 +464,7 @@ class Link:
|
|||
for resource in self.outgoing_resources:
|
||||
resource.cancel()
|
||||
if self._channel:
|
||||
self._channel.shutdown()
|
||||
self._channel._shutdown()
|
||||
|
||||
self.prv = None
|
||||
self.pub = None
|
||||
|
@ -801,7 +801,7 @@ class Link:
|
|||
RNS.log(f"Channel data received without open channel", RNS.LOG_DEBUG)
|
||||
else:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
self._channel.receive(plaintext)
|
||||
self._channel._receive(plaintext)
|
||||
packet.prove()
|
||||
|
||||
elif packet.packet_type == RNS.Packet.PROOF:
|
||||
|
|
|
@ -153,7 +153,7 @@ class ProtocolHarness(contextlib.AbstractContextManager):
|
|||
self.channel = Channel(self.outlet)
|
||||
|
||||
def cleanup(self):
|
||||
self.channel.shutdown()
|
||||
self.channel._shutdown()
|
||||
|
||||
def __exit__(self, __exc_type: typing.Type[BaseException], __exc_value: BaseException,
|
||||
__traceback: types.TracebackType) -> bool:
|
||||
|
@ -282,7 +282,7 @@ class TestChannel(unittest.TestCase):
|
|||
self.h.channel.add_message_handler(handler2)
|
||||
envelope = RNS.Channel.Envelope(self.h.outlet, message, sequence=0)
|
||||
raw = envelope.pack()
|
||||
self.h.channel.receive(raw)
|
||||
self.h.channel._receive(raw)
|
||||
|
||||
self.assertEqual(1, handler1_called)
|
||||
self.assertEqual(0, handler2_called)
|
||||
|
@ -290,7 +290,7 @@ class TestChannel(unittest.TestCase):
|
|||
handler1_return = False
|
||||
envelope = RNS.Channel.Envelope(self.h.outlet, message, sequence=1)
|
||||
raw = envelope.pack()
|
||||
self.h.channel.receive(raw)
|
||||
self.h.channel._receive(raw)
|
||||
|
||||
self.assertEqual(2, handler1_called)
|
||||
self.assertEqual(1, handler2_called)
|
||||
|
@ -348,7 +348,7 @@ class TestChannel(unittest.TestCase):
|
|||
self.assertFalse(envelope.tracked)
|
||||
self.assertEqual(0, len(decoded))
|
||||
|
||||
self.h.channel.receive(packet.raw)
|
||||
self.h.channel._receive(packet.raw)
|
||||
|
||||
self.assertEqual(1, len(decoded))
|
||||
|
||||
|
|
Loading…
Reference in New Issue