-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #83 from pedohorse/multiple-interface-listeners
Multiple interface listeners
- Loading branch information
Showing
27 changed files
with
969 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import asyncio | ||
from .component_base import ComponentBase | ||
from .logging import get_logger | ||
from multiprocessing import Process, get_context | ||
from multiprocessing.connection import Connection | ||
from threading import Event | ||
|
||
from typing import Optional, Tuple | ||
|
||
|
||
def rx_recv(rx, ev: Event): | ||
while not rx.poll(0.1): | ||
if ev.is_set(): | ||
return None | ||
return rx.recv() | ||
|
||
|
||
async def target_async(component: ComponentBase, rx: Connection, log_level: int): | ||
logger = get_logger(f'detached_component.{type(component).__name__}') | ||
logger.setLevel(log_level) | ||
logger.debug('component starting...') | ||
await component.start() | ||
logger.debug('component started') | ||
|
||
exit_ev = Event() | ||
stop_task = asyncio.get_event_loop().run_in_executor(None, rx_recv, rx, exit_ev) | ||
done_task = asyncio.create_task(component.wait_till_stops()) | ||
done, _ = await asyncio.wait( | ||
[ | ||
done_task, | ||
stop_task | ||
], | ||
return_when=asyncio.FIRST_COMPLETED, | ||
) | ||
if done_task in done: | ||
exit_ev.set() | ||
if stop_task in done: | ||
await stop_task # reraise exceptions if any happened | ||
else: | ||
stop_task.cancel() | ||
elif stop_task in done: | ||
logger.debug('component received stop message') | ||
component.stop() | ||
logger.debug('component stop called') | ||
await done_task | ||
else: | ||
raise RuntimeError('unreachable') | ||
rx.close() | ||
logger.debug('component finished') | ||
|
||
|
||
def target(component: ComponentBase, rx: Connection, log_level: int): | ||
asyncio.run(target_async(component, rx, log_level)) | ||
|
||
|
||
class ComponentProcessWrapper: | ||
_context = get_context('spawn') | ||
|
||
def __init__(self, component_to_run: ComponentBase): | ||
""" | ||
component_to_run must not be started | ||
""" | ||
self.__component = component_to_run | ||
self.__proc: Optional[Process] = None | ||
self.__comm_sender: Optional[Connection] = None | ||
|
||
async def start(self): | ||
rx, tx = self._context.Pipe(False) # type: Tuple[Connection, Connection] | ||
|
||
self.__comm_sender = tx | ||
log_level = get_logger('detached_component').level | ||
self.__proc = self._context.Process(target=target, args=(self.__component, rx, log_level)) | ||
self.__proc.start() | ||
|
||
def stop(self): | ||
if self.__proc is None: | ||
raise RuntimeError('not started') | ||
if not self.__comm_sender.closed: | ||
try: | ||
self.__comm_sender.send(0) | ||
except OSError: # rx might close beforehand | ||
pass | ||
self.__comm_sender.close() | ||
|
||
async def wait_till_stops(self): | ||
if self.__proc is None: | ||
raise RuntimeError('not started') | ||
# better poll for now, | ||
# alternative would be using a dedicated 1-thread pool executor and wait there | ||
while self.__proc.exitcode is None: | ||
# what is this random polling time? | ||
await asyncio.sleep(2.5) | ||
if not self.__comm_sender.closed: | ||
self.__comm_sender.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from .address import DirectAddress, AddressChain | ||
from .exceptions import MessageTransferError | ||
from typing import Iterable, Optional | ||
|
||
|
||
class RoutingImpossible(MessageTransferError): | ||
def __init__(self, sources: Iterable[DirectAddress], destination: AddressChain, *, wrapped_exception: Optional[Exception] = None): | ||
self.sources = list(sources) | ||
self.destination = destination | ||
super().__init__(f'failed to find suitable address to reach {self.destination} from {self.sources}', wrapped_exception=wrapped_exception) | ||
|
||
|
||
class AddressRouter: | ||
def select_source_for(self, possible_sources: Iterable[DirectAddress], destination: AddressChain) -> DirectAddress: | ||
raise NotImplementedError() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import socket | ||
from ..address import DirectAddress, AddressChain | ||
from ..address_routing import AddressRouter, RoutingImpossible | ||
|
||
from typing import Iterable | ||
|
||
|
||
class IPRouter(AddressRouter): | ||
def __init__(self, use_caching: bool = True): | ||
# WE ASSUME ROUTING TO BE STATIC | ||
# So if a case where interfaces/routing may change dynamically come up - | ||
# then we can think about them, not now | ||
if use_caching: | ||
self.__routing_cache = {} | ||
else: | ||
self.__routing_cache = None | ||
|
||
def select_source_for(self, possible_sources: Iterable[DirectAddress], destination: AddressChain) -> DirectAddress: | ||
""" | ||
gets interface ipv4 address to reach given address | ||
""" | ||
# we expect address to be ip:port | ||
destination0 = destination.split_address()[0] | ||
if ':' in destination0: | ||
dest_ip, _ = destination0.split(':', 1) | ||
else: | ||
dest_ip = str(destination0) | ||
|
||
do_caching = self.__routing_cache is not None | ||
cache_key = None | ||
if do_caching: | ||
possible_sources = tuple(sorted(possible_sources)) | ||
# cache key takes all input arguments into account | ||
# NOTE: we don't take destination port into account | ||
cache_key = (possible_sources, dest_ip) | ||
elif not isinstance(possible_sources, tuple): | ||
possible_sources = tuple(possible_sources) | ||
|
||
if not do_caching or cache_key not in self.__routing_cache: | ||
# thank you https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib | ||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
try: | ||
# doesn't even have to be reachable | ||
s.connect((dest_ip, 1)) | ||
myip = s.getsockname()[0] | ||
except Exception as e: | ||
raise RoutingImpossible(possible_sources, destination, wrapped_exception=e) | ||
finally: | ||
s.close() | ||
|
||
candidates = [ | ||
x | ||
for x in possible_sources | ||
if myip == (x.split(':', 1)[0] if ':' in x else x) | ||
] | ||
if len(candidates) == 0: | ||
raise RoutingImpossible(possible_sources, destination) | ||
# there may be several candidates, and we may add some more logic to pick one from them in future | ||
|
||
if do_caching: | ||
assert cache_key is not None | ||
self.__routing_cache[cache_key] = candidates[0] | ||
else: | ||
return candidates[0] | ||
|
||
assert do_caching and cache_key is not None | ||
return self.__routing_cache[cache_key] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.