import json import socket import uuid from pathlib import Path from typing import Any, Callable try: from zeroconf import ServiceInfo, Zeroconf ZEROCONF_AVAILABLE = True except ImportError: ZEROCONF_AVAILABLE = False Zeroconf = None ServiceInfo = None class DiscoveryService: SERVICE_TYPE = "_snippets._tcp.local." SERVICE_NAME = "snip" PEER_CACHE_FILE = Path.home() / ".snip" / "peers.json" def __init__(self, port: int = 8765, peer_name: str | None = None): self.port = port self.peer_id = str(uuid.uuid4())[:8] self.peer_name = peer_name or socket.gethostname() self._zeroconf: Zeroconf | None = None self._service_info: ServiceInfo | None = None self._listening = False self._listeners: list[Callable[[dict[str, Any]], None]] = [] self._peer_cache: list[dict[str, Any]] = [] self._load_peer_cache() def _load_peer_cache(self): if self.PEER_CACHE_FILE.exists(): try: with open(self.PEER_CACHE_FILE) as f: self._peer_cache = json.load(f) except Exception: self._peer_cache = [] def _save_peer_cache(self): self.PEER_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True) try: with open(self.PEER_CACHE_FILE, "w") as f: json.dump(self._peer_cache, f) except Exception: pass def register(self) -> bool: if not ZEROCONF_AVAILABLE: return False try: self._zeroconf = Zeroconf() self._service_info = ServiceInfo( self.SERVICE_TYPE, f"{self.SERVICE_NAME}_{self.peer_id}.{self.SERVICE_TYPE}", addresses=[socket.inet_aton(socket.gethostbyname(socket.gethostname()))], port=self.port, properties={ "peer_id": self.peer_id, "peer_name": self.peer_name, }, ) self._zeroconf.register_service(self._service_info) return True except Exception: return False def unregister(self): if self._zeroconf and self._service_info: try: self._zeroconf.unregister_service(self._service_info) except Exception: pass self._zeroconf.close() self._zeroconf = None self._service_info = None def discover_peers(self, timeout: float = 5.0) -> list[dict[str, Any]]: if not ZEROCONF_AVAILABLE: return self._peer_cache peers = [] seen_ids = set() try: zeroconf = Zeroconf() listener = PeerListener(seen_ids, peers) browser = zeroconf.ServiceBrowser(self.SERVICE_TYPE, listener) browser.cancel() for peer in self._peer_cache: if peer["peer_id"] not in seen_ids: seen_ids.add(peer["peer_id"]) zeroconf.close() except Exception: pass self._peer_cache = peers self._save_peer_cache() return peers def add_listener(self, callback: Callable[[dict[str, Any]], None]): self._listeners.append(callback) def remove_listener(self, callback: Callable[[dict[str, Any]], None]): if callback in self._listeners: self._listeners.remove(callback) class PeerListener: def __init__(self, seen_ids: set, peers: list): self.seen_ids = seen_ids self.peers = peers def add_service(self, zeroconf, service_type: str, name: str): try: info = zeroconf.get_service_info(service_type, name) if info and info.properties: peer_id = info.properties.get(b"peer_id", b"").decode() peer_name = info.properties.get(b"peer_name", b"").decode() if peer_id and peer_id not in self.seen_ids: self.seen_ids.add(peer_id) addresses = [ socket.inet_ntoa(addr) for addr in info.addresses ] if info.addresses else [] self.peers.append({ "peer_id": peer_id, "peer_name": peer_name, "addresses": addresses, "port": info.port, }) except Exception: pass def remove_service(self, zeroconf, service_type: str, name: str): pass