|
@@ -1,4 +1,5 @@
|
|
|
import asyncio
|
|
|
+import secrets
|
|
|
from copy import deepcopy
|
|
|
from dataclasses import dataclass
|
|
|
from importlib.resources import path
|
|
@@ -10,7 +11,7 @@ from multiaddr import Multiaddr
|
|
|
|
|
|
import hivemind.hivemind_cli as cli
|
|
|
import hivemind.p2p.p2p_daemon_bindings.p2pclient as p2pclient
|
|
|
-from hivemind.p2p.p2p_daemon_bindings.datastructures import PeerID, StreamInfo
|
|
|
+from hivemind.p2p.p2p_daemon_bindings.datastructures import PeerID, PeerInfo, StreamInfo
|
|
|
from hivemind.proto import p2pd_pb2
|
|
|
from hivemind.utils import MSGPackSerializer
|
|
|
from hivemind.utils.logging import get_logger
|
|
@@ -20,8 +21,6 @@ logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
P2PD_FILENAME = 'p2pd'
|
|
|
-NUM_RETRIES = 3
|
|
|
-RETRY_DELAY = 0.4
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
@@ -63,20 +62,22 @@ class P2P:
|
|
|
}
|
|
|
|
|
|
def __init__(self):
|
|
|
+ self.id = None
|
|
|
self._child = None
|
|
|
self._alive = False
|
|
|
self._listen_task = None
|
|
|
self._server_stopped = asyncio.Event()
|
|
|
|
|
|
@classmethod
|
|
|
- async def create(cls, *args, quic: bool = True, tls: bool = True, conn_manager: bool = True,
|
|
|
+ async def create(cls, *args, quic: bool = False, tls: bool = True, conn_manager: bool = True,
|
|
|
dht_mode: str = 'dht_server', force_reachability: Optional[str] = None,
|
|
|
nat_port_map: bool = True, auto_nat: bool = True,
|
|
|
- bootstrap_peers: Optional[List[Multiaddr]] = None,
|
|
|
- use_ipfs: bool = False, external_port: int = None,
|
|
|
- daemon_listen_port: int = None, use_relay: bool = True, use_relay_hop: bool = False,
|
|
|
+ bootstrap_peers: Optional[List[Multiaddr]] = None, use_ipfs: bool = False,
|
|
|
+ host_maddrs: Optional[List[Multiaddr]] = None,
|
|
|
+ use_relay: bool = True, use_relay_hop: bool = False,
|
|
|
use_relay_discovery: bool = False, use_auto_relay: bool = False, relay_hop_limit: int = 0,
|
|
|
- **kwargs) -> 'P2P':
|
|
|
+ quiet: bool = True,
|
|
|
+ ping_n_retries: int = 3, ping_retry_delay: float = 0.4, **kwargs) -> 'P2P':
|
|
|
"""
|
|
|
Start a new p2pd process and connect to it.
|
|
|
:param quic: Enables the QUIC transport
|
|
@@ -89,13 +90,13 @@ class P2P:
|
|
|
:param bootstrap: Connects to bootstrap peers and bootstraps the dht if enabled
|
|
|
:param bootstrap_peers: List of bootstrap peers
|
|
|
:param use_ipfs: Bootstrap to IPFS (works only if bootstrap=True and bootstrap_peers=None)
|
|
|
- :param external_port: port for external connections from other p2p instances
|
|
|
- :param daemon_listen_port: port for connection daemon and client binding
|
|
|
+ :param host_maddrs: multiaddresses for external connections from other p2p instances
|
|
|
:param use_relay: enables circuit relay
|
|
|
:param use_relay_hop: enables hop for relay
|
|
|
:param use_relay_discovery: enables passive discovery for relay
|
|
|
:param use_auto_relay: enables autorelay
|
|
|
:param relay_hop_limit: sets the hop limit for hop relays
|
|
|
+ :param quiet: make the daemon process quiet
|
|
|
:param args: positional CLI arguments for the p2p daemon
|
|
|
:param kwargs: keyword CLI arguments for the p2p daemon
|
|
|
:return: a wrapper for the p2p daemon
|
|
@@ -108,40 +109,59 @@ class P2P:
|
|
|
with path(cli, P2PD_FILENAME) as p:
|
|
|
p2pd_path = p
|
|
|
|
|
|
+ socket_uid = secrets.token_urlsafe(8)
|
|
|
+ self._daemon_listen_maddr = Multiaddr(f'/unix/tmp/hivemind-p2pd-{socket_uid}.sock')
|
|
|
+ self._client_listen_maddr = Multiaddr(f'/unix/tmp/hivemind-p2pclient-{socket_uid}.sock')
|
|
|
+
|
|
|
need_bootstrap = bool(bootstrap_peers) or use_ipfs
|
|
|
bootstrap_peers = cls._make_bootstrap_peers(bootstrap_peers)
|
|
|
dht = cls.DHT_MODE_MAPPING.get(dht_mode, {'dht': 0})
|
|
|
force_reachability = cls.FORCE_REACHABILITY_MAPPING.get(force_reachability, {})
|
|
|
+ host_maddrs = {'hostAddrs': ','.join(str(maddr) for maddr in host_maddrs)} if host_maddrs else {}
|
|
|
proc_args = self._make_process_args(
|
|
|
str(p2pd_path), *args,
|
|
|
+ listen=self._daemon_listen_maddr,
|
|
|
quic=quic, tls=tls, connManager=conn_manager,
|
|
|
natPortMap=nat_port_map, autonat=auto_nat,
|
|
|
relay=use_relay, relayHop=use_relay_hop, relayDiscovery=use_relay_discovery,
|
|
|
autoRelay=use_auto_relay, relayHopLimit=relay_hop_limit,
|
|
|
- b=need_bootstrap, **{**bootstrap_peers, **dht, **force_reachability, **kwargs})
|
|
|
- self._assign_daemon_ports(external_port, daemon_listen_port)
|
|
|
+ b=need_bootstrap, q=quiet, **{**bootstrap_peers, **dht, **force_reachability, **host_maddrs, **kwargs})
|
|
|
+
|
|
|
+ self._initialize(proc_args)
|
|
|
+ await self._ping_daemon_with_retries(ping_n_retries, ping_retry_delay)
|
|
|
+
|
|
|
+ return self
|
|
|
+
|
|
|
+ def _initialize(self, proc_args: List[str]) -> None:
|
|
|
+ self._child = Popen(args=proc_args, encoding="utf8")
|
|
|
+ self._alive = True
|
|
|
+ self._client = p2pclient.Client(self._daemon_listen_maddr, self._client_listen_maddr)
|
|
|
+
|
|
|
+ async def _ping_daemon_with_retries(self, ping_n_retries: int, ping_retry_delay: float) -> None:
|
|
|
+ for try_number in range(ping_n_retries):
|
|
|
+ await asyncio.sleep(ping_retry_delay * (2 ** try_number))
|
|
|
+
|
|
|
+ if self._child.poll() is not None: # Process died
|
|
|
+ break
|
|
|
|
|
|
- for try_count in range(NUM_RETRIES):
|
|
|
try:
|
|
|
- self._initialize(proc_args)
|
|
|
- await self._wait_for_client(RETRY_DELAY * (2 ** try_count))
|
|
|
+ await self._ping_daemon()
|
|
|
break
|
|
|
except Exception as e:
|
|
|
- logger.debug(f"Failed to initialize p2p daemon: {e}")
|
|
|
- self._terminate()
|
|
|
- if try_count == NUM_RETRIES - 1:
|
|
|
+ if try_number == ping_n_retries - 1:
|
|
|
+ logger.error(f'Failed to ping p2pd: {e}')
|
|
|
+ await self.shutdown()
|
|
|
raise
|
|
|
- self._assign_daemon_ports()
|
|
|
|
|
|
- return self
|
|
|
+ if self._child.returncode is not None:
|
|
|
+ raise RuntimeError(f'The p2p daemon has died with return code {self._child.returncode}')
|
|
|
|
|
|
@classmethod
|
|
|
- async def replicate(cls, daemon_listen_port: int, external_port: int) -> 'P2P':
|
|
|
+ async def replicate(cls, daemon_listen_maddr: Multiaddr) -> 'P2P':
|
|
|
"""
|
|
|
Connect to existing p2p daemon
|
|
|
- :param daemon_listen_port: port for connection daemon and client binding
|
|
|
- :param external_port: port for external connections from other p2p instances
|
|
|
- :return: new wrapper for existing p2p daemon
|
|
|
+ :param daemon_listen_maddr: multiaddr of the existing p2p daemon
|
|
|
+ :return: new wrapper for the existing p2p daemon
|
|
|
"""
|
|
|
|
|
|
self = cls()
|
|
@@ -149,14 +169,20 @@ class P2P:
|
|
|
# Use external already running p2pd
|
|
|
self._child = None
|
|
|
self._alive = True
|
|
|
- self._assign_daemon_ports(external_port, daemon_listen_port)
|
|
|
- self._client_listen_port = find_open_port()
|
|
|
- self._client = p2pclient.Client(
|
|
|
- Multiaddr(f'/ip4/127.0.0.1/tcp/{self._daemon_listen_port}'),
|
|
|
- Multiaddr(f'/ip4/127.0.0.1/tcp/{self._client_listen_port}'))
|
|
|
- await self._wait_for_client()
|
|
|
+
|
|
|
+ socket_uid = secrets.token_urlsafe(8)
|
|
|
+ self._daemon_listen_maddr = daemon_listen_maddr
|
|
|
+ self._client_listen_maddr = Multiaddr(f'/unix/tmp/hivemind-p2pclient-{socket_uid}.sock')
|
|
|
+
|
|
|
+ self._client = p2pclient.Client(self._daemon_listen_maddr, self._client_listen_maddr)
|
|
|
+
|
|
|
+ await self._ping_daemon()
|
|
|
return self
|
|
|
|
|
|
+ async def _ping_daemon(self) -> None:
|
|
|
+ self.id, maddrs = await self._client.identify()
|
|
|
+ logger.debug(f'Launched p2pd with id = {self.id}, host multiaddrs = {maddrs}')
|
|
|
+
|
|
|
async def identify_maddrs(self) -> List[Multiaddr]:
|
|
|
_, maddrs = await self._client.identify()
|
|
|
if not maddrs:
|
|
@@ -165,6 +191,9 @@ class P2P:
|
|
|
p2p_maddr = Multiaddr(f'/p2p/{self.id.to_base58()}')
|
|
|
return [addr.encapsulate(p2p_maddr) for addr in maddrs]
|
|
|
|
|
|
+ async def list_peers(self) -> List[PeerInfo]:
|
|
|
+ return list(await self._client.list_peers())
|
|
|
+
|
|
|
async def wait_for_at_least_n_peers(self, n_peers: int, attempts: int = 3, delay: float = 1) -> None:
|
|
|
for _ in range(attempts):
|
|
|
peers = await self._client.list_peers()
|
|
@@ -174,36 +203,9 @@ class P2P:
|
|
|
|
|
|
raise RuntimeError('Not enough peers')
|
|
|
|
|
|
- def _initialize(self, proc_args: List[str]) -> None:
|
|
|
- proc_args = deepcopy(proc_args)
|
|
|
- proc_args.extend(self._make_process_args(
|
|
|
- hostAddrs=f'/ip4/0.0.0.0/tcp/{self._external_port},/ip4/0.0.0.0/udp/{self._external_port}/quic',
|
|
|
- listen=f'/ip4/127.0.0.1/tcp/{self._daemon_listen_port}'
|
|
|
- ))
|
|
|
- self._child = Popen(args=proc_args, encoding="utf8")
|
|
|
- self._alive = True
|
|
|
- self._client_listen_port = find_open_port()
|
|
|
- self._client = p2pclient.Client(
|
|
|
- Multiaddr(f'/ip4/127.0.0.1/tcp/{self._daemon_listen_port}'),
|
|
|
- Multiaddr(f'/ip4/127.0.0.1/tcp/{self._client_listen_port}'))
|
|
|
-
|
|
|
- async def _wait_for_client(self, delay: float = 0) -> None:
|
|
|
- await asyncio.sleep(delay)
|
|
|
- self.id, _ = await self._client.identify()
|
|
|
-
|
|
|
- def _assign_daemon_ports(self, external_port: int = None, daemon_listen_port: int = None) -> None:
|
|
|
- if external_port is None:
|
|
|
- external_port = find_open_port()
|
|
|
- if daemon_listen_port is None:
|
|
|
- daemon_listen_port = find_open_port()
|
|
|
- while daemon_listen_port == external_port:
|
|
|
- daemon_listen_port = find_open_port()
|
|
|
-
|
|
|
- self._external_port, self._daemon_listen_port = external_port, daemon_listen_port
|
|
|
-
|
|
|
@property
|
|
|
- def external_port(self) -> int:
|
|
|
- return self._external_port
|
|
|
+ def daemon_listen_maddr(self) -> Multiaddr:
|
|
|
+ return self._daemon_listen_maddr
|
|
|
|
|
|
@staticmethod
|
|
|
async def send_raw_data(data: bytes, writer: asyncio.StreamWriter) -> None:
|
|
@@ -314,14 +316,14 @@ class P2P:
|
|
|
|
|
|
return do_handle_unary_stream
|
|
|
|
|
|
- def start_listening(self) -> None:
|
|
|
+ def _start_listening(self) -> None:
|
|
|
async def listen() -> None:
|
|
|
async with self._client.listen():
|
|
|
await self._server_stopped.wait()
|
|
|
|
|
|
self._listen_task = asyncio.create_task(listen())
|
|
|
|
|
|
- async def stop_listening(self) -> None:
|
|
|
+ async def _stop_listening(self) -> None:
|
|
|
if self._listen_task is not None:
|
|
|
self._server_stopped.set()
|
|
|
self._listen_task.cancel()
|
|
@@ -333,13 +335,13 @@ class P2P:
|
|
|
|
|
|
async def add_stream_handler(self, name: str, handle: Callable[[bytes], bytes]) -> None:
|
|
|
if self._listen_task is None:
|
|
|
- self.start_listening()
|
|
|
+ self._start_listening()
|
|
|
await self._client.stream_handler(name, self._handle_stream(handle))
|
|
|
|
|
|
async def add_unary_handler(self, name: str, handle: Callable[[Any, P2PContext], Any],
|
|
|
in_proto_type: type, out_proto_type: type) -> None:
|
|
|
if self._listen_task is None:
|
|
|
- self.start_listening()
|
|
|
+ self._start_listening()
|
|
|
await self._client.stream_handler(
|
|
|
name, self._handle_unary_stream(handle, name, in_proto_type, out_proto_type))
|
|
|
|
|
@@ -372,6 +374,7 @@ class P2P:
|
|
|
return self._alive
|
|
|
|
|
|
async def shutdown(self) -> None:
|
|
|
+ await self._stop_listening()
|
|
|
await asyncio.get_event_loop().run_in_executor(None, self._terminate)
|
|
|
|
|
|
def _terminate(self) -> None:
|
|
@@ -379,6 +382,7 @@ class P2P:
|
|
|
if self._child is not None and self._child.poll() is None:
|
|
|
self._child.terminate()
|
|
|
self._child.wait()
|
|
|
+ logger.debug(f'Terminated p2pd with id = {self.id}')
|
|
|
|
|
|
@staticmethod
|
|
|
def _make_process_args(*args, **kwargs) -> List[str]:
|