generated from Dicken/dickendock
round55: DNS-Abfragen mit Timeout absichern, Thread-Pool-Groesse erhoehen (behebt Haenger nach masscan/httpx-Vorabscan), Live-Haekchen pro fertigem Geraet im Batch
This commit is contained in:
@@ -31,6 +31,21 @@ ENV NODE_ENV=production
|
|||||||
ENV DATABASE_PATH=/data/launchpad.db
|
ENV DATABASE_PATH=/data/launchpad.db
|
||||||
ENV PORT=3001
|
ENV PORT=3001
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
|
# node:dns/promises (lookup/reverse, siehe scanner/dns.ts) läuft NICHT über
|
||||||
|
# normales nicht-blockierendes I/O, sondern über getaddrinfo() im
|
||||||
|
# Betriebssystem - Node schickt das über den libuv-Threadpool ab, der
|
||||||
|
# standardmäßig nur 4 Threads hat. Beim Sammel-Scan mit bis zu 10 parallel
|
||||||
|
# gescannten Geräten (siehe routes/scan.ts CONCURRENT_DEVICES) und je bis zu
|
||||||
|
# 4 DNS-Abfragen pro Gerät reichte das nicht annähernd und führte zu einem
|
||||||
|
# Rückstau, der wie ein hängender Sammel-Scan aussah (siehe Bugreport - Scan
|
||||||
|
# blieb nach erfolgreichem masscan-/httpx-Vorabscan minutenlang in der
|
||||||
|
# eigentlichen Geräte-Schleife stehen). MUSS hier als Umgebungsvariable
|
||||||
|
# gesetzt werden, nicht im JS-Code selbst: das Projekt läuft als natives ESM
|
||||||
|
# ("type": "module"), dort werden alle import-Anweisungen VOR jeglichem
|
||||||
|
# sonstigen Code im Modul ausgeführt (auch wenn dieser Code textuell davor
|
||||||
|
# steht) - ein Setzen von process.env.UV_THREADPOOL_SIZE am Dateianfang von
|
||||||
|
# index.ts käme damit zu spät.
|
||||||
|
ENV UV_THREADPOOL_SIZE=32
|
||||||
|
|
||||||
# iputils-ping stellt einen "echten" ping-Befehl mit ICMP-Unterstützung
|
# iputils-ping stellt einen "echten" ping-Befehl mit ICMP-Unterstützung
|
||||||
# bereit (nicht nur die BusyBox-Variante) - für den optionalen Live-Status-
|
# bereit (nicht nur die BusyBox-Variante) - für den optionalen Live-Status-
|
||||||
|
|||||||
@@ -372,7 +372,12 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const batch = devices.slice(i, i + CONCURRENT_DEVICES);
|
const batch = devices.slice(i, i + CONCURRENT_DEVICES);
|
||||||
updateJobProgress("devices", scannedCount, batch.map((d) => d.hostname).join(", "));
|
const stillRunning = new Set(batch.map((d) => d.hostname));
|
||||||
|
|
||||||
|
const renderBatchLabel = () =>
|
||||||
|
`${batch.map((d) => (stillRunning.has(d.hostname) ? d.hostname : `✓ ${d.hostname}`)).join(", ")}`;
|
||||||
|
|
||||||
|
updateJobProgress("devices", scannedCount, renderBatchLabel());
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
batch.map(async (device) => {
|
batch.map(async (device) => {
|
||||||
@@ -382,6 +387,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// Geräte-Seite) - überspringen statt zu überlappen, sonst
|
// Geräte-Seite) - überspringen statt zu überlappen, sonst
|
||||||
// könnten beide gleichzeitig denselben neuen Dienst anlegen
|
// könnten beide gleichzeitig denselben neuen Dienst anlegen
|
||||||
// wollen.
|
// wollen.
|
||||||
|
stillRunning.delete(device.hostname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -428,6 +434,15 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
unlockDevice(device.id);
|
unlockDevice(device.id);
|
||||||
|
// Live sichtbar machen, WELCHES Gerät im aktuellen Batch
|
||||||
|
// schon fertig ist ("✓ Name") und welche noch laufen -
|
||||||
|
// bisher aktualisierte sich die Anzeige erst wieder, wenn
|
||||||
|
// ALLE 10 Geräte des Batches fertig waren. Blieb eines davon
|
||||||
|
// hängen, sah der komplette Batch wie eingefroren aus, ohne
|
||||||
|
// erkennbar zu machen, WELCHES Gerät das eigentlich betraf
|
||||||
|
// (siehe Bugreport).
|
||||||
|
stillRunning.delete(device.hostname);
|
||||||
|
updateJobProgress("devices", scannedCount, renderBatchLabel());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,19 +5,57 @@ export interface DnsResolution {
|
|||||||
ip: string;
|
ip: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GEFUNDENER FLASCHENHALS (round55): dns.lookup()/dns.reverse() aus
|
||||||
|
// node:dns/promises laufen NICHT direkt asynchron über den Netzwerk-Stack,
|
||||||
|
// sondern über getaddrinfo() im Betriebssystem - Node schickt das über den
|
||||||
|
// libuv-Threadpool ab, der standardmäßig nur 4 Threads hat
|
||||||
|
// (UV_THREADPOOL_SIZE=4), UNABHÄNGIG von Node's eigentlich nicht-blockierendem
|
||||||
|
// I/O-Modell. Ohne eigenen Timeout kann eine einzelne langsame/hängende
|
||||||
|
// Abfrage (z. B. für Handys/Smart-Geräte mit FritzBox-Spitznamen wie
|
||||||
|
// "S24-Ultra-von-Sebastian", die weder per normaler DNS noch per .home/.local
|
||||||
|
// auflösbar sind, aber je nach Resolver/Musl-libc im Alpine-Image erst nach
|
||||||
|
// langer Zeit "nicht gefunden" zurückmelden) einen der 4 Threads über
|
||||||
|
// Sekunden bis Minuten blockieren. Bei bis zu 10 Geräten gleichzeitig (siehe
|
||||||
|
// routes/scan.ts CONCURRENT_DEVICES) mit je bis zu 4 DNS-Abfragen (3x
|
||||||
|
// resolveHostname-Varianten + 1x reverseLookup) stauen sich die restlichen
|
||||||
|
// Abfragen dann VOR dem 4er-Nadelöhr - das erklärt einen Sammel-Scan, der
|
||||||
|
// nach einem erfolgreichen masscan-/httpx-Vorabscan plötzlich minutenlang in
|
||||||
|
// der eigentlichen Geräte-Schleife hängt, obwohl dort eigentlich nur noch
|
||||||
|
// leichte HTTP-Detailabfragen laufen sollten (siehe Bugreport).
|
||||||
|
const DNS_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => reject(new Error("DNS-Zeitüberschreitung")), ms);
|
||||||
|
promise.then(
|
||||||
|
(value) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(value);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Versucht der Spezifikation folgend: hostname, hostname.home, hostname.local.
|
* Versucht der Spezifikation folgend: hostname, hostname.home, hostname.local.
|
||||||
* Gibt die erste erfolgreich aufgelöste Variante zurück, sonst null.
|
* Gibt die erste erfolgreich aufgelöste Variante zurück, sonst null. Jeder
|
||||||
|
* einzelne Versuch ist auf DNS_TIMEOUT_MS begrenzt (siehe Kommentar oben) -
|
||||||
|
* eine hängende Abfrage blockiert damit höchstens 3 × 1,5s statt potenziell
|
||||||
|
* unbegrenzt.
|
||||||
*/
|
*/
|
||||||
export async function resolveHostname(shortName: string): Promise<DnsResolution | null> {
|
export async function resolveHostname(shortName: string): Promise<DnsResolution | null> {
|
||||||
const candidates = [shortName, `${shortName}.home`, `${shortName}.local`];
|
const candidates = [shortName, `${shortName}.home`, `${shortName}.local`];
|
||||||
|
|
||||||
for (const hostname of candidates) {
|
for (const hostname of candidates) {
|
||||||
try {
|
try {
|
||||||
const { address } = await lookup(hostname);
|
const { address } = await withTimeout(lookup(hostname), DNS_TIMEOUT_MS);
|
||||||
return { hostname, ip: address };
|
return { hostname, ip: address };
|
||||||
} catch {
|
} catch {
|
||||||
// nächste Variante versuchen
|
// nächste Variante versuchen (Fehler ODER Timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +67,12 @@ export async function resolveHostname(shortName: string): Promise<DnsResolution
|
|||||||
* Viele Router/DHCP-Server (FritzBox, Pi-hole, AdGuard Home, ...) tragen hier
|
* Viele Router/DHCP-Server (FritzBox, Pi-hole, AdGuard Home, ...) tragen hier
|
||||||
* automatisch den vom Gerät gemeldeten DHCP-Hostnamen ein, auch wenn die
|
* automatisch den vom Gerät gemeldeten DHCP-Hostnamen ein, auch wenn die
|
||||||
* normale Vorwärtsauflösung (Name -> IP) nichts findet. Nützlich als
|
* normale Vorwärtsauflösung (Name -> IP) nichts findet. Nützlich als
|
||||||
* zusätzliche Quelle für Gerätenamen, siehe scanner/networkScanner.ts.
|
* zusätzliche Quelle für Gerätenamen, siehe scanner/networkScanner.ts. Mit
|
||||||
|
* demselben Timeout abgesichert wie resolveHostname() oben.
|
||||||
*/
|
*/
|
||||||
export async function reverseLookup(ip: string): Promise<string | null> {
|
export async function reverseLookup(ip: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const names = await reverse(ip);
|
const names = await withTimeout(reverse(ip), DNS_TIMEOUT_MS);
|
||||||
return names[0] ?? null;
|
return names[0] ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user