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):