#!/data/data/com.termux/files/usr/bin/python3 """mdns: unified CLI for mDNS resolve / browse / publish / reverse / cache. Loads lib/mdns_tools/ from the repo root. Usable both from an install into ~/.local/bin (installer sets a symlink pointing at the repo checkout) and directly from a clone. """ import argparse import json import os import sys from pathlib import Path _ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(_ROOT / "lib")) from mdns_tools import __version__ # noqa: E402 def _cmd_resolve(a: argparse.Namespace) -> int: from mdns_tools import resolve as _r, cache as _c ip = _r.resolve(a.host, family=a.family, timeout=a.timeout, retries=a.retries, use_cache=not a.no_cache) if ip: print(ip) return 0 if a.stale: ip = _c.get_stale(a.host if a.host.endswith(".local") else f"{a.host}.local", family=a.family) if ip: print(ip) return 0 print(f"error: could not resolve '{a.host}'", file=sys.stderr) return 1 def _cmd_reverse(a: argparse.Namespace) -> int: from mdns_tools import reverse as _rev name = _rev.reverse(a.ip, timeout=a.timeout, retries=a.retries) if name: print(name) return 0 print(f"error: no PTR for {a.ip}", file=sys.stderr) return 1 def _cmd_browse(a: argparse.Namespace) -> int: from mdns_tools import browse as _br # loads zeroconf if a.list_types: for t in _br.list_types(duration=a.duration): print(t) return 0 items = _br.browse(service_type=a.type, duration=a.duration) if a.json: print(json.dumps(items, indent=2)) return 0 for it in items: addrs = ", ".join(it.get("addresses") or []) or "-" print(f'{it["type"]:<30} {it.get("server","-"):<28} {addrs}:{it.get("port","-")}') return 0 def _cmd_publish(a: argparse.Namespace) -> int: from mdns_tools import publish as _p # loads zeroconf _p.run(config_path=a.config, hostname=a.hostname, verbose=not a.quiet) return 0 def _cmd_cache(a: argparse.Namespace) -> int: from mdns_tools import cache as _c if a.op == "list": for k, v in _c.all_entries().items(): host, fam = k.split("|") if "|" in k else (k, "v4") print(f'{host:<40} {fam} {v.get("ip"):<40} ttl={v.get("ttl")}') elif a.op == "clear": n = _c.clear(a.host) print(f"removed {n} entries") return 0 def main() -> int: p = argparse.ArgumentParser(prog="mdns", description="mDNS / DNS-SD tools") p.add_argument("--version", action="version", version=f"mdns {__version__}") sub = p.add_subparsers(dest="cmd", required=True) r = sub.add_parser("resolve", help="hostname -> IP") r.add_argument("host") r.add_argument("--family", choices=["v4", "v6"], default="v4") r.add_argument("--timeout", type=float, default=1.5) r.add_argument("--retries", type=int, default=2) r.add_argument("--no-cache", action="store_true") r.add_argument("--stale", action="store_true", help="accept expired cache as last resort") r.set_defaults(func=_cmd_resolve) rv = sub.add_parser("reverse", help="IP -> hostname (PTR)") rv.add_argument("ip") rv.add_argument("--timeout", type=float, default=1.5) rv.add_argument("--retries", type=int, default=2) rv.set_defaults(func=_cmd_reverse) b = sub.add_parser("browse", help="discover services on the LAN") b.add_argument("--type", help="e.g. _ssh._tcp.local. (default: all types)") b.add_argument("--duration", type=float, default=3.0) b.add_argument("--json", action="store_true") b.add_argument("--list-types", action="store_true") b.set_defaults(func=_cmd_browse) pu = sub.add_parser("publish", help="advertise this device + services") pu.add_argument("--config", help="path to services YAML/JSON") pu.add_argument("--hostname") pu.add_argument("--quiet", action="store_true") pu.set_defaults(func=_cmd_publish) ca = sub.add_parser("cache", help="inspect/clear the resolver cache") ca.add_argument("op", choices=["list", "clear"]) ca.add_argument("host", nargs="?") ca.set_defaults(func=_cmd_cache) args = p.parse_args() return args.func(args) if __name__ == "__main__": sys.exit(main())