336 lines
9.9 KiB
Python
336 lines
9.9 KiB
Python
|
|
"""
|
||
|
|
mDNS service registration for GSPro Remote.
|
||
|
|
Allows the application to be discovered on the local network.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import logging
|
||
|
|
import socket
|
||
|
|
import threading
|
||
|
|
from typing import Optional, Dict, Any
|
||
|
|
|
||
|
|
try:
|
||
|
|
from zeroconf import ServiceInfo, Zeroconf, IPVersion
|
||
|
|
except ImportError as e:
|
||
|
|
raise ImportError(f"Zeroconf library not installed: {e}")
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class MDNSService:
|
||
|
|
"""
|
||
|
|
Manages mDNS/Bonjour service registration for network discovery.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
name: str = "gsproapp",
|
||
|
|
port: int = 5005,
|
||
|
|
service_type: str = "_http._tcp.local.",
|
||
|
|
properties: Optional[Dict[str, Any]] = None,
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Initialize mDNS service.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
name: Service name (will be accessible as {name}.local)
|
||
|
|
port: Port number the service is running on
|
||
|
|
service_type: mDNS service type
|
||
|
|
properties: Additional service properties
|
||
|
|
"""
|
||
|
|
self.name = name
|
||
|
|
self.port = port
|
||
|
|
self.service_type = service_type
|
||
|
|
self.properties = properties or {}
|
||
|
|
|
||
|
|
self.zeroconf: Optional[Zeroconf] = None
|
||
|
|
self.service_info: Optional[ServiceInfo] = None
|
||
|
|
self.is_running = False
|
||
|
|
self._lock = threading.Lock()
|
||
|
|
|
||
|
|
def _get_local_ip(self) -> str:
|
||
|
|
"""
|
||
|
|
Get the local IP address of the machine.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Local IP address as string
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# Create a socket to determine the local IP
|
||
|
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||
|
|
# Connect to a public DNS server to determine local interface
|
||
|
|
s.connect(("8.8.8.8", 80))
|
||
|
|
return s.getsockname()[0]
|
||
|
|
except Exception:
|
||
|
|
# Fallback to localhost if can't determine
|
||
|
|
return "127.0.0.1"
|
||
|
|
|
||
|
|
def _create_service_info(self) -> ServiceInfo:
|
||
|
|
"""
|
||
|
|
Create the ServiceInfo object for registration.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Configured ServiceInfo object
|
||
|
|
"""
|
||
|
|
local_ip = self._get_local_ip()
|
||
|
|
hostname = socket.gethostname()
|
||
|
|
|
||
|
|
# Create fully qualified service name
|
||
|
|
service_name = f"{self.name}.{self.service_type}"
|
||
|
|
|
||
|
|
# Add default properties
|
||
|
|
default_properties = {
|
||
|
|
"version": "0.1.0",
|
||
|
|
"platform": "windows",
|
||
|
|
"api": "rest",
|
||
|
|
"ui": "web",
|
||
|
|
}
|
||
|
|
|
||
|
|
# Merge with custom properties
|
||
|
|
all_properties = {**default_properties, **self.properties}
|
||
|
|
|
||
|
|
# Convert properties to bytes
|
||
|
|
properties_bytes = {}
|
||
|
|
for key, value in all_properties.items():
|
||
|
|
if isinstance(value, str):
|
||
|
|
properties_bytes[key] = value.encode("utf-8")
|
||
|
|
elif isinstance(value, bytes):
|
||
|
|
properties_bytes[key] = value
|
||
|
|
else:
|
||
|
|
properties_bytes[key] = str(value).encode("utf-8")
|
||
|
|
|
||
|
|
# Create service info
|
||
|
|
service_info = ServiceInfo(
|
||
|
|
type_=self.service_type,
|
||
|
|
name=service_name,
|
||
|
|
addresses=[socket.inet_aton(local_ip)],
|
||
|
|
port=self.port,
|
||
|
|
properties=properties_bytes,
|
||
|
|
server=f"{hostname}.local.",
|
||
|
|
)
|
||
|
|
|
||
|
|
return service_info
|
||
|
|
|
||
|
|
def start(self) -> bool:
|
||
|
|
"""
|
||
|
|
Start the mDNS service registration.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if service started successfully, False otherwise
|
||
|
|
"""
|
||
|
|
with self._lock:
|
||
|
|
if self.is_running:
|
||
|
|
logger.warning("mDNS service is already running")
|
||
|
|
return True
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Create Zeroconf instance
|
||
|
|
self.zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
|
||
|
|
|
||
|
|
# Create and register service
|
||
|
|
self.service_info = self._create_service_info()
|
||
|
|
self.zeroconf.register_service(self.service_info)
|
||
|
|
|
||
|
|
self.is_running = True
|
||
|
|
|
||
|
|
logger.info(f"mDNS service registered: {self.name}.local:{self.port} (type: {self.service_type})")
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to start mDNS service: {e}")
|
||
|
|
self.cleanup()
|
||
|
|
return False
|
||
|
|
|
||
|
|
def stop(self) -> None:
|
||
|
|
"""Stop the mDNS service registration."""
|
||
|
|
with self._lock:
|
||
|
|
if not self.is_running:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.cleanup()
|
||
|
|
self.is_running = False
|
||
|
|
logger.info("mDNS service stopped")
|
||
|
|
|
||
|
|
def cleanup(self) -> None:
|
||
|
|
"""Clean up mDNS resources."""
|
||
|
|
try:
|
||
|
|
if self.zeroconf and self.service_info:
|
||
|
|
self.zeroconf.unregister_service(self.service_info)
|
||
|
|
|
||
|
|
if self.zeroconf:
|
||
|
|
self.zeroconf.close()
|
||
|
|
self.zeroconf = None
|
||
|
|
|
||
|
|
self.service_info = None
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error during mDNS cleanup: {e}")
|
||
|
|
|
||
|
|
def update_properties(self, properties: Dict[str, Any]) -> bool:
|
||
|
|
"""
|
||
|
|
Update service properties.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
properties: New properties to set
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if properties updated successfully, False otherwise
|
||
|
|
"""
|
||
|
|
with self._lock:
|
||
|
|
if not self.is_running:
|
||
|
|
logger.warning("Cannot update properties: service is not running")
|
||
|
|
return False
|
||
|
|
|
||
|
|
try:
|
||
|
|
self.properties.update(properties)
|
||
|
|
|
||
|
|
# Recreate and re-register service with new properties
|
||
|
|
if self.zeroconf and self.service_info:
|
||
|
|
self.zeroconf.unregister_service(self.service_info)
|
||
|
|
self.service_info = self._create_service_info()
|
||
|
|
self.zeroconf.register_service(self.service_info)
|
||
|
|
|
||
|
|
logger.info("mDNS service properties updated")
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to update mDNS properties: {e}")
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
def get_url(self) -> str:
|
||
|
|
"""
|
||
|
|
Get the URL for accessing the service.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Service URL
|
||
|
|
"""
|
||
|
|
return f"http://{self.name}.local:{self.port}"
|
||
|
|
|
||
|
|
def __enter__(self):
|
||
|
|
"""Context manager entry."""
|
||
|
|
self.start()
|
||
|
|
return self
|
||
|
|
|
||
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
|
|
"""Context manager exit."""
|
||
|
|
self.stop()
|
||
|
|
|
||
|
|
|
||
|
|
class MDNSBrowser:
|
||
|
|
"""
|
||
|
|
Browse for mDNS services on the network.
|
||
|
|
Useful for discovering other GSPro Remote instances.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, service_type: str = "_http._tcp.local."):
|
||
|
|
"""
|
||
|
|
Initialize mDNS browser.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
service_type: Type of services to browse for
|
||
|
|
"""
|
||
|
|
self.service_type = service_type
|
||
|
|
self.services: Dict[str, Dict[str, Any]] = {}
|
||
|
|
self.zeroconf: Optional[Zeroconf] = None
|
||
|
|
|
||
|
|
def browse(self, timeout: float = 5.0) -> Dict[str, Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Browse for services on the network.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
timeout: Time to wait for services (seconds)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary of discovered services
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
from zeroconf import ServiceBrowser, ServiceListener
|
||
|
|
import time
|
||
|
|
|
||
|
|
class Listener(ServiceListener):
|
||
|
|
def __init__(self, browser):
|
||
|
|
self.browser = browser
|
||
|
|
|
||
|
|
def add_service(self, zeroconf, service_type, name):
|
||
|
|
info = zeroconf.get_service_info(service_type, name)
|
||
|
|
if info:
|
||
|
|
self.browser.services[name] = {
|
||
|
|
"name": name,
|
||
|
|
"address": socket.inet_ntoa(info.addresses[0]) if info.addresses else None,
|
||
|
|
"port": info.port,
|
||
|
|
"properties": info.properties,
|
||
|
|
}
|
||
|
|
|
||
|
|
def remove_service(self, zeroconf, service_type, name):
|
||
|
|
self.browser.services.pop(name, None)
|
||
|
|
|
||
|
|
def update_service(self, zeroconf, service_type, name):
|
||
|
|
pass
|
||
|
|
|
||
|
|
self.zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
|
||
|
|
listener = Listener(self)
|
||
|
|
browser = ServiceBrowser(self.zeroconf, self.service_type, listener)
|
||
|
|
|
||
|
|
# Wait for services to be discovered
|
||
|
|
time.sleep(timeout)
|
||
|
|
|
||
|
|
browser.cancel()
|
||
|
|
self.zeroconf.close()
|
||
|
|
|
||
|
|
return self.services
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to browse for services: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
|
||
|
|
# Test function for development
|
||
|
|
def test_mdns_service():
|
||
|
|
"""Test mDNS service registration."""
|
||
|
|
import time
|
||
|
|
|
||
|
|
print("Testing mDNS service registration...")
|
||
|
|
|
||
|
|
# Test service registration
|
||
|
|
service = MDNSService(
|
||
|
|
name="gsproapp-test",
|
||
|
|
port=5005,
|
||
|
|
properties={"test": "true", "instance": "development"},
|
||
|
|
)
|
||
|
|
|
||
|
|
if service.start():
|
||
|
|
print(f"✓ mDNS service started: {service.get_url()}")
|
||
|
|
print(f" You should be able to access it at: http://gsproapp-test.local:5005")
|
||
|
|
|
||
|
|
# Keep service running for 10 seconds
|
||
|
|
print(" Service will run for 10 seconds...")
|
||
|
|
time.sleep(10)
|
||
|
|
|
||
|
|
# Test property update
|
||
|
|
if service.update_properties({"status": "running", "uptime": "10s"}):
|
||
|
|
print("✓ Properties updated successfully")
|
||
|
|
|
||
|
|
service.stop()
|
||
|
|
print("✓ mDNS service stopped")
|
||
|
|
else:
|
||
|
|
print("✗ Failed to start mDNS service")
|
||
|
|
|
||
|
|
# Test service browsing
|
||
|
|
print("\nBrowsing for HTTP services on the network...")
|
||
|
|
browser = MDNSBrowser()
|
||
|
|
services = browser.browse(timeout=3.0)
|
||
|
|
|
||
|
|
if services:
|
||
|
|
print(f"Found {len(services)} services:")
|
||
|
|
for name, info in services.items():
|
||
|
|
print(f" - {name}: {info['address']}:{info['port']}")
|
||
|
|
else:
|
||
|
|
print("No services found")
|
||
|
|
|
||
|
|
print("\nmDNS test complete!")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
test_mdns_service()
|