diff --git a/diagnostic/index.html b/diagnostic/index.html index e1a6401..d3d7375 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -2763,7 +2763,7 @@ return; } if (msg.type === 'sat_devices') { - if (msg.satellite) { satDevices[msg.satellite] = { devices: msg.devices || [], location: msg.location, ts: Date.now() }; } + if (msg.satellite) { satDevices[msg.satellite] = { devices: msg.devices || [], location: msg.location, ts: Date.now() }; delete satScanError[msg.satellite]; } satScanning = null; renderSatellites(); return; @@ -5634,6 +5634,7 @@ let satellites = []; // [{id, location, caps, control, online}] let satDevices = {}; // id → {devices, location, ts} let satScanning = null; // id des gerade scannenden Satelliten + const satScanError = {}; // id → Meldung, wenn ein Scan nicht beantwortet wurde let satCreds = {}; // satId → { ip → [typen] } (welche Creds gesetzt sind, OHNE Secrets) let credsModalSat = ''; // aktuell im Modal bearbeiteter Satellit let credsModalIp = ''; @@ -6161,10 +6162,20 @@ }).join(''); } + // Antwortet der Satellit nicht, haengt "Scanne Netz …" sonst fuer immer. + // Der Satellit selbst bricht einen Scan nach 90 s ab (SCAN_MAX_SEC). function scanSatellite(id) { satScanning = id; + delete satScanError[id]; renderSatellites(); send({ action: 'sat_discover', satellite: id }); + setTimeout(() => { + if (satScanning !== id) return; + satScanning = null; + satScanError[id] = 'Keine Antwort nach 2 Minuten. Der Satellit ist verbunden, beantwortet den Scan aber nicht — ' + + 'vermutlich hängt er. Satellit neu starten (bzw. aktualisieren) hilft.'; + renderSatellites(); + }, 120000); } /** Anzeigenamen fuer einen Satelliten oder Host-Agenten vergeben. @@ -6218,6 +6229,8 @@ let devHtml; if (scanning) { devHtml = '
Scanne Netz …
'; + } else if (satScanError[s.id]) { + devHtml = '
⚠ ' + escapeHtml(satScanError[s.id]) + '
'; } else if (dev) { if (!dev.devices.length) { devHtml = '
Keine Geraete gefunden.
'; diff --git a/satellite/satellite.py b/satellite/satellite.py index c7e0269..ee40f5c 100644 --- a/satellite/satellite.py +++ b/satellite/satellite.py @@ -181,6 +181,9 @@ APK_MAX_BYTES = int(os.environ.get("APK_MAX_BYTES", str(64 * 1024 * 1024)) or st SCAN_INTERVAL_SEC = int(os.environ.get("SCAN_INTERVAL_SEC", "300") or "300") DISCOVER_TIMEOUT_SEC = float(os.environ.get("DISCOVER_TIMEOUT_SEC", "6") or "6") +# Obergrenze fuer einen ganzen Scan. Haengt ein Schritt (mDNS, SNMP, ADB …), +# kommt nach dieser Zeit die letzte bekannte Liste zurueck statt gar nichts. +SCAN_MAX_SEC = float(os.environ.get("SCAN_MAX_SEC", "90") or "90") DEVICE_CACHE_TTL_SEC = int(os.environ.get("DEVICE_CACHE_TTL_SEC", "120") or "120") # http.get/http.post: Body-Ausschnitt. Default grosszuegig (ganze Statusseiten @@ -1797,6 +1800,7 @@ class Satellite: self._devices: list[dict] = [] self._devices_ts: float = 0.0 self._scanning = False + self._scan_stage = "" # Offene Port-Tunnel: tunnelId -> {"writer", "task", "last", "ziel"} self._tunnels: dict[str, dict] = {} @@ -1812,26 +1816,40 @@ class Satellite: break return self._devices self._scanning = True + self._scan_stage = "start" try: - loop = asyncio.get_event_loop() - mdns = await loop.run_in_executor(None, _discover_mdns, DISCOVER_TIMEOUT_SEC) - ssdp = await loop.run_in_executor(None, _discover_ssdp, DISCOVER_TIMEOUT_SEC) - arp = await loop.run_in_executor(None, _discover_arp) - _self = [_self_device()] if LIST_SELF else [] - _self = [d for d in _self if d] - self._devices = _apply_names(_merge_devices(mdns, ssdp, arp, _self)) - if SNMP_DISCOVERY: - await self._enrich_snmp(self._devices) - await self._enrich_adb(self._devices) - self._devices_ts = time.time() - n_snmp = sum(1 for d in self._devices if d.get("snmpCapable")) - n_adb = sum(1 for d in self._devices if d.get("adbCapable")) - logger.info("[scan] %d Geraete (mdns=%d ssdp=%d arp=%d, snmp=%d, adb=%d)", - len(self._devices), len(mdns), len(ssdp), len(arp), n_snmp, n_adb) + await asyncio.wait_for(self._scan_inner(), SCAN_MAX_SEC) + except asyncio.TimeoutError: + logger.warning("[scan] nach %.0fs abgebrochen — haengt bei: %s. Liefere die letzte Liste (%d Geraete).", + SCAN_MAX_SEC, self._scan_stage, len(self._devices)) finally: self._scanning = False return self._devices + async def _scan_inner(self) -> None: + """Ein kompletter Scan; _scan begrenzt ihn auf SCAN_MAX_SEC.""" + loop = asyncio.get_event_loop() + self._scan_stage = "mdns" + mdns = await loop.run_in_executor(None, _discover_mdns, DISCOVER_TIMEOUT_SEC) + self._scan_stage = "ssdp" + ssdp = await loop.run_in_executor(None, _discover_ssdp, DISCOVER_TIMEOUT_SEC) + self._scan_stage = "arp" + arp = await loop.run_in_executor(None, _discover_arp) + _self = [_self_device()] if LIST_SELF else [] + _self = [d for d in _self if d] + devices = _apply_names(_merge_devices(mdns, ssdp, arp, _self)) + if SNMP_DISCOVERY: + self._scan_stage = "snmp" + await self._enrich_snmp(devices) + self._scan_stage = "adb" + await self._enrich_adb(devices) + self._devices = devices + self._devices_ts = time.time() + n_snmp = sum(1 for d in self._devices if d.get("snmpCapable")) + n_adb = sum(1 for d in self._devices if d.get("adbCapable")) + logger.info("[scan] %d Geraete (mdns=%d ssdp=%d arp=%d, snmp=%d, adb=%d)", + len(self._devices), len(mdns), len(ssdp), len(arp), n_snmp, n_adb) + async def _enrich_snmp(self, devices: list) -> None: """Fragt jedes Geraet mit IP parallel per SNMP-System-Group ab und haengt die Infos an. Verbessert Name/Typ, wenn bisher nur eine IP bekannt war.""" @@ -2020,6 +2038,16 @@ class Satellite: return True return target in (SATELLITE_ID.lower(), SATELLITE_LOCATION.lower()) + async def _discover_reply(self, payload: dict) -> None: + devices = await self._scan(force=bool(payload.get("force"))) + await self._send({ + "type": "sat_devices", + "payload": {"requestId": payload.get("requestId", ""), "satellite": SATELLITE_ID, + "location": SATELLITE_LOCATION, "devices": devices, + "net": NET}, + "timestamp": int(time.time() * 1000), + }) + async def _handle(self, raw: str) -> None: try: msg = json.loads(raw) @@ -2031,15 +2059,9 @@ class Satellite: if mtype == "sat_discover": if not self._for_me(payload): return - req_id = payload.get("requestId", "") - devices = await self._scan(force=bool(payload.get("force"))) - await self._send({ - "type": "sat_devices", - "payload": {"requestId": req_id, "satellite": SATELLITE_ID, - "location": SATELLITE_LOCATION, "devices": devices, - "net": NET}, - "timestamp": int(time.time() * 1000), - }) + # Eigener Task: ein langer Scan darf den Nachrichten-Loop (Tunnel, + # Befehle, weitere Scans) nicht anhalten. + asyncio.create_task(self._discover_reply(payload)) elif mtype == "sat_tunnel_open": if not self._for_me(payload):