mdns-termux/lib/mdns_tools/service.py

111 lines
3.2 KiB
Python
Raw Permalink Normal View History

feat: publisher-daemon (singleton + re-announce), sync-hosts, status/stop y tests Bloque de robustez operativa + integracion de sistema + calidad: Publisher como servicio (lib/mdns_tools/service.py): - `mdns publish --daemon`: daemon singleton con lock PID en ~/.cache/mdns/publisher.pid. Un segundo intento es rechazado -> evita el problema de "Termux SMB" duplicado (varios publishers compitiendo por el mismo nombre, que zeroconf renombra a "-2"). - Re-anuncio automatico al cambiar la IP local (WiFi<->datos, roaming): unregister + register con la nueva IP para que el registro no quede stale. - Apagado limpio: maneja SIGTERM ademas de SIGINT (loop en ticks de 1s) y envia goodbye packets para que los clientes borren la entrada al instante. - `mdns status` (running/host/ip/servicios/desde) y `mdns stop`. Integracion de sistema (lib/mdns_tools/hosts.py): - `mdns sync-hosts [--dry-run]`: escribe un bloque gestionado en $PREFIX/etc/hosts (o /etc/hosts) para que ping/curl/kubectl nativos resuelvan .local sin el wrapper `resolve`. Solo toca su propio bloque. Refactor publish.py: expone helpers reutilizables (local_ipv4, load_config, build_infos, register_all, unregister_all) que consume el daemon. Tests offline (tests/, pytest, 20 casos, sin red): - construccion/parseo de queries A, seleccion de IP ante multi-homing (rechazo de rangos virtuales), cache con TTL y familias, PTR reverse, render/strip del bloque de hosts. - pyproject.toml con metadata, deps, extras [test] y config de pytest. README: seccion de daemon, sync-hosts y tests; tabla RFC ampliada. bashrc.snippet: aliases mdns-up/status/down/hosts (consolidados). install.sh: fix de backticks; symlinks ya cubren mdns + lib via resolve(). Nota: publish.py (109) y service.py (110) exceden levemente las 100 lineas; se dejan como modulos cohesivos de responsabilidad unica, igual que el dispatcher CLI, para no fragmentar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-20 17:59:10 +00:00
"""Long-running publisher: singleton PID lock, clean SIGTERM/SIGINT goodbye,
and automatic re-announce when the device's IP changes (WiFi<->data, roaming).
"""
import json
import os
import signal
import time
from pathlib import Path
from zeroconf import Zeroconf
from . import publish as _p
PID_FILE = Path.home() / ".cache" / "mdns" / "publisher.pid"
_STOP = False
def _read_pid() -> dict | None:
if not PID_FILE.exists():
return None
try:
d = json.loads(PID_FILE.read_text())
except Exception:
return None
try:
os.kill(int(d["pid"]), 0) # probe liveness
except (OSError, ValueError, KeyError):
return None
return d
def _write_pid(host: str, ip: str, n: int) -> None:
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
PID_FILE.write_text(json.dumps({
"pid": os.getpid(), "host": host, "ip": ip, "services": n,
"since": time.strftime("%Y-%m-%d %H:%M:%S"),
}))
def status() -> int:
d = _read_pid()
if not d:
print("publisher: stopped")
return 1
print(f"publisher: running (pid {d['pid']})\n"
f" host: {d.get('host')}.local\n"
f" ip: {d.get('ip')}\n"
f" services: {d.get('services', 0)}\n"
f" since: {d.get('since')}")
return 0
def _handle_stop(signum, frame) -> None:
global _STOP
_STOP = True
def run_daemon(config_path: str | None = None, hostname: str | None = None,
interval: int = 15, verbose: bool = True) -> int:
existing = _read_pid()
if existing:
print(f"error: publisher already running (pid {existing['pid']}). "
f"Use `mdns stop` first.")
return 1
signal.signal(signal.SIGTERM, _handle_stop)
signal.signal(signal.SIGINT, _handle_stop)
cfg = _p.load_config(config_path)
host = hostname or cfg.get("hostname", "movil")
ip = _p.local_ipv4()
zc = Zeroconf()
infos = _p.build_infos(cfg, host, ip)
_write_pid(host, ip, len(infos)) # mark alive before the slow probing register
_p.register_all(zc, infos)
if verbose:
print(f"[mdns] daemon publishing {host}.local ({ip}), {len(infos)} record(s)")
try:
elapsed = 0
while not _STOP:
time.sleep(1) # short tick so SIGTERM is honored within ~1s
elapsed += 1
if elapsed < interval:
continue
elapsed = 0
new_ip = _p.local_ipv4()
if new_ip != ip and new_ip != "127.0.0.1":
if verbose:
print(f"[mdns] IP changed {ip} -> {new_ip}, re-announcing")
_p.unregister_all(zc, infos)
ip = new_ip
infos = _p.build_infos(cfg, host, ip)
_p.register_all(zc, infos)
_write_pid(host, ip, len(infos))
finally:
_p.unregister_all(zc, infos)
zc.close()
PID_FILE.unlink(missing_ok=True)
if verbose:
print("[mdns] daemon stopped, goodbye packets sent.")
return 0
def stop() -> int:
d = _read_pid()
if not d:
print("publisher: not running")
return 1
os.kill(int(d["pid"]), signal.SIGTERM)
print(f"sent stop to pid {d['pid']}")
return 0