generated from Dicken/dickendock
round30: alle drei Scanner als Hintergrund-Job, Dienst-Duplikate strukturell verhindert (Migration+Constraint+Geraete-Sperre), zweiter Erkennungsversuch bei leerem Ersttreffer
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
isJobRunning,
|
||||
isCancelRequested,
|
||||
requestCancel,
|
||||
tryLockDevice,
|
||||
unlockDevice,
|
||||
} from "../scanJobs.js";
|
||||
import type { Device } from "@launchpad/shared";
|
||||
|
||||
@@ -141,6 +143,10 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(404).send({ error: "Gerät nicht gefunden" });
|
||||
}
|
||||
|
||||
if (!tryLockDevice(device.id)) {
|
||||
return reply.code(409).send({ error: "Für dieses Gerät läuft bereits ein Scan." });
|
||||
}
|
||||
|
||||
try {
|
||||
return await performDeviceScan(device);
|
||||
} catch (err) {
|
||||
@@ -153,6 +159,8 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
});
|
||||
request.log.error(err);
|
||||
return reply.code(500).send({ error: "Geräte-Scan fehlgeschlagen", detail });
|
||||
} finally {
|
||||
unlockDevice(device.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -199,6 +207,14 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
break;
|
||||
}
|
||||
updateJobProgress("devices", scannedCount, device.hostname);
|
||||
if (!tryLockDevice(device.id)) {
|
||||
// Für dieses eine Gerät läuft bereits ein separater Scan (z. B.
|
||||
// über den "Jetzt scannen"-Button auf der Geräte-Seite) -
|
||||
// überspringen statt zu überlappen, sonst könnten beide
|
||||
// gleichzeitig denselben neuen Dienst anlegen wollen.
|
||||
scannedCount++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = await performDeviceScan(device);
|
||||
created += result.created;
|
||||
@@ -208,6 +224,8 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
allNew.push(...result.newServices);
|
||||
} catch {
|
||||
// einzelnes fehlgeschlagenes Gerät soll den Rest nicht abbrechen
|
||||
} finally {
|
||||
unlockDevice(device.id);
|
||||
}
|
||||
scannedCount++;
|
||||
}
|
||||
@@ -280,7 +298,11 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] {
|
||||
// FritzBox-Scan: liest die Geräteliste per TR-064 und legt/aktualisiert Geräte.
|
||||
// Erfordert FRITZBOX_HOST / FRITZBOX_USERNAME / FRITZBOX_PASSWORD (optional
|
||||
// FRITZBOX_PORT, Default 49000) als Umgebungsvariablen.
|
||||
app.post("/api/scan/fritzbox", async (request, reply) => {
|
||||
app.get("/api/scan/fritzbox/status", async () => {
|
||||
return getJobStatus("fritzbox");
|
||||
});
|
||||
|
||||
app.post("/api/scan/fritzbox", async (_request, reply) => {
|
||||
const host = process.env.FRITZBOX_HOST;
|
||||
const username = process.env.FRITZBOX_USERNAME;
|
||||
const password = process.env.FRITZBOX_PASSWORD;
|
||||
@@ -292,83 +314,94 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] {
|
||||
});
|
||||
}
|
||||
|
||||
const port = process.env.FRITZBOX_PORT ? Number(process.env.FRITZBOX_PORT) : 49000;
|
||||
|
||||
try {
|
||||
const devicesBeforeScan = deviceRepo.listDevices().filter((d) => d.source === "fritzbox");
|
||||
|
||||
const hosts = await fetchFritzBoxHosts({ host, port, username, password });
|
||||
// Mehrere TR-064-Einträge für dasselbe physische Gerät (gleiche MAC)
|
||||
// werden VOR dem Upsert zusammengeführt (siehe mergeFritzboxHosts oben) -
|
||||
// deshalb kann "gefundene Einträge" > Anzahl eindeutiger Geräte sein.
|
||||
const mergedHosts = mergeFritzboxHosts(hosts);
|
||||
const results = mergedHosts.map((h) =>
|
||||
deviceRepo.upsertDeviceFromScan({
|
||||
hostname: h.hostname,
|
||||
ip: h.ip,
|
||||
mac: h.mac,
|
||||
online: h.online,
|
||||
source: "fritzbox",
|
||||
})
|
||||
);
|
||||
|
||||
const devices = results.map((r) => r.device);
|
||||
|
||||
// Vorschläge für abweichende Hostnamen/IPs bereits bekannter Geräte
|
||||
// einsammeln (siehe upsertDeviceFromScan) - werden NICHT automatisch
|
||||
// übernommen, nur zur Bestätigung zurückgegeben.
|
||||
const deviceChanges = results.flatMap((r) =>
|
||||
r.changes.map((c) => ({ deviceId: r.device.id, deviceHostname: r.device.hostname, ...c }))
|
||||
);
|
||||
|
||||
// Geräte, die die FritzBox früher gemeldet hatte, diesmal aber nicht
|
||||
// mehr in der Liste sind – werden erst nach mindestens
|
||||
// staleDeviceThresholdDays Tagen Abwesenheit zur Durchsicht
|
||||
// vorgeschlagen (nicht schon beim ersten verpassten Scan), damit ein
|
||||
// kurzzeitig offline/im Standby befindliches Gerät (Reboot,
|
||||
// WLAN-Aussetzer, Nacht-Standby) nicht sofort zum Löschen
|
||||
// vorgeschlagen wird. lastScan wird bei jedem Fund aktualisiert (siehe
|
||||
// upsertDeviceFromScan) - bleibt es stehen, war das Gerät seither nicht
|
||||
// mehr da.
|
||||
//
|
||||
// "Gefunden" wird über die ID der in DIESEM Scan tatsächlich
|
||||
// upgeserteten Geräte geprüft (nicht über einen IP-Abgleich!): ändert
|
||||
// sich die IP eines Geräts (z. B. DHCP-Neuvergabe), wird es trotzdem
|
||||
// korrekt über seine MAC wiedererkannt und upgeserted - ein reiner
|
||||
// IP-Vergleich hätte es fälschlich als "nicht mehr gefunden" gemeldet,
|
||||
// obwohl es im selben Scan noch da war, nur unter neuer IP.
|
||||
const foundDeviceIds = new Set(devices.map((d) => d.id));
|
||||
const staleDeviceThresholdDays = Number(settingsRepo.listSettings().staleDeviceThresholdDays ?? 7);
|
||||
const staleThresholdMs = staleDeviceThresholdDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const staleDevices = devicesBeforeScan.filter((d) => {
|
||||
if (foundDeviceIds.has(d.id)) return false;
|
||||
const lastSeen = d.lastScan ? new Date(d.lastScan).getTime() : 0;
|
||||
return now - lastSeen >= staleThresholdMs;
|
||||
});
|
||||
|
||||
logRepo.logScan({
|
||||
type: "fritzbox",
|
||||
level: "info",
|
||||
message: `FritzBox-Scan: ${hosts.length} Eintrag/Einträge (${devices.length} eindeutige Geräte)${staleDevices.length > 0 ? `, ${staleDevices.length} nicht mehr gemeldet` : ""}${deviceChanges.length > 0 ? `, ${deviceChanges.length} Änderung(en) vorgeschlagen` : ""}`,
|
||||
});
|
||||
|
||||
return {
|
||||
found: hosts.length,
|
||||
uniqueDevices: devices.length,
|
||||
devices,
|
||||
staleDevices,
|
||||
deviceChanges,
|
||||
};
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
logRepo.logScan({
|
||||
type: "fritzbox",
|
||||
level: "error",
|
||||
message: `FritzBox-Scan fehlgeschlagen – ${detail}`,
|
||||
});
|
||||
request.log.error(err);
|
||||
return reply.code(502).send({ error: "FritzBox-Scan fehlgeschlagen", detail });
|
||||
if (isJobRunning("fritzbox")) {
|
||||
return reply.code(409).send({ error: "Es läuft bereits ein FritzBox-Scan." });
|
||||
}
|
||||
|
||||
const port = process.env.FRITZBOX_PORT ? Number(process.env.FRITZBOX_PORT) : 49000;
|
||||
startJob("fritzbox", 1);
|
||||
|
||||
// Wie bei den anderen Scannern: läuft im Hintergrund weiter, die
|
||||
// HTTP-Antwort kommt sofort - macht den Status auch hier
|
||||
// geräteübergreifend abfragbar statt nur im Browser sichtbar, der ihn
|
||||
// gestartet hat.
|
||||
void (async () => {
|
||||
try {
|
||||
const devicesBeforeScan = deviceRepo.listDevices().filter((d) => d.source === "fritzbox");
|
||||
|
||||
const hosts = await fetchFritzBoxHosts({ host, port, username, password });
|
||||
// Mehrere TR-064-Einträge für dasselbe physische Gerät (gleiche MAC)
|
||||
// werden VOR dem Upsert zusammengeführt (siehe mergeFritzboxHosts oben) -
|
||||
// deshalb kann "gefundene Einträge" > Anzahl eindeutiger Geräte sein.
|
||||
const mergedHosts = mergeFritzboxHosts(hosts);
|
||||
const results = mergedHosts.map((h) =>
|
||||
deviceRepo.upsertDeviceFromScan({
|
||||
hostname: h.hostname,
|
||||
ip: h.ip,
|
||||
mac: h.mac,
|
||||
online: h.online,
|
||||
source: "fritzbox",
|
||||
})
|
||||
);
|
||||
|
||||
const devices = results.map((r) => r.device);
|
||||
|
||||
// Vorschläge für abweichende Hostnamen/IPs bereits bekannter Geräte
|
||||
// einsammeln (siehe upsertDeviceFromScan) - werden NICHT automatisch
|
||||
// übernommen, nur zur Bestätigung zurückgegeben.
|
||||
const deviceChanges = results.flatMap((r) =>
|
||||
r.changes.map((c) => ({ deviceId: r.device.id, deviceHostname: r.device.hostname, ...c }))
|
||||
);
|
||||
|
||||
// Geräte, die die FritzBox früher gemeldet hatte, diesmal aber nicht
|
||||
// mehr in der Liste sind – werden erst nach mindestens
|
||||
// staleDeviceThresholdDays Tagen Abwesenheit zur Durchsicht
|
||||
// vorgeschlagen (nicht schon beim ersten verpassten Scan), damit ein
|
||||
// kurzzeitig offline/im Standby befindliches Gerät (Reboot,
|
||||
// WLAN-Aussetzer, Nacht-Standby) nicht sofort zum Löschen
|
||||
// vorgeschlagen wird. lastScan wird bei jedem Fund aktualisiert (siehe
|
||||
// upsertDeviceFromScan) - bleibt es stehen, war das Gerät seither nicht
|
||||
// mehr da.
|
||||
//
|
||||
// "Gefunden" wird über die ID der in DIESEM Scan tatsächlich
|
||||
// upgeserteten Geräte geprüft (nicht über einen IP-Abgleich!): ändert
|
||||
// sich die IP eines Geräts (z. B. DHCP-Neuvergabe), wird es trotzdem
|
||||
// korrekt über seine MAC wiedererkannt und upgeserted - ein reiner
|
||||
// IP-Vergleich hätte es fälschlich als "nicht mehr gefunden" gemeldet,
|
||||
// obwohl es im selben Scan noch da war, nur unter neuer IP.
|
||||
const foundDeviceIds = new Set(devices.map((d) => d.id));
|
||||
const staleDeviceThresholdDays = Number(settingsRepo.listSettings().staleDeviceThresholdDays ?? 7);
|
||||
const staleThresholdMs = staleDeviceThresholdDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const staleDevices = devicesBeforeScan.filter((d) => {
|
||||
if (foundDeviceIds.has(d.id)) return false;
|
||||
const lastSeen = d.lastScan ? new Date(d.lastScan).getTime() : 0;
|
||||
return now - lastSeen >= staleThresholdMs;
|
||||
});
|
||||
|
||||
logRepo.logScan({
|
||||
type: "fritzbox",
|
||||
level: "info",
|
||||
message: `FritzBox-Scan: ${hosts.length} Eintrag/Einträge (${devices.length} eindeutige Geräte)${staleDevices.length > 0 ? `, ${staleDevices.length} nicht mehr gemeldet` : ""}${deviceChanges.length > 0 ? `, ${deviceChanges.length} Änderung(en) vorgeschlagen` : ""}`,
|
||||
});
|
||||
|
||||
finishJob("fritzbox", {
|
||||
found: hosts.length,
|
||||
uniqueDevices: devices.length,
|
||||
staleDevices,
|
||||
deviceChanges,
|
||||
});
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
logRepo.logScan({
|
||||
type: "fritzbox",
|
||||
level: "error",
|
||||
message: `FritzBox-Scan fehlgeschlagen – ${detail}`,
|
||||
});
|
||||
failJob("fritzbox", detail);
|
||||
}
|
||||
})();
|
||||
|
||||
return reply.code(202).send({ started: true });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user