feat(desktop, network): Fehlerhandling verbessert, Tools-Menü erweitert und mDNS-Discovery optimiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+2
-1
@@ -40,7 +40,8 @@ app {
|
||||
jvm-options = [
|
||||
"-Xms128m",
|
||||
"-Xmx512m",
|
||||
"-Dfile.encoding=UTF-8"
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"--enable-native-access=ALL-UNNAMED"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Curator
|
||||
last_update: 2026-05-07
|
||||
---
|
||||
|
||||
# 2026-05-07 — Session Log (Frontend Networking, Discovery, Connectivity)
|
||||
|
||||
## Kontext
|
||||
- Fokus: Stabilisierung der lokalen Host/Client‑Kommunikation (mDNS, WS‑Chat), robuste Connectivity‑Checks, UX für Backup‑Pfad, Session‑Abschluss mit Dokumentation.
|
||||
|
||||
## Summary
|
||||
- ConnectivityCheck robuster gemacht (Fallbacks, schneller Erstcheck) und Logs (Base‑URL, WS‑Port) korrigiert.
|
||||
- Discovery/Registration zentralisiert und entdoppelt; Interface‑Bindung und Logging verbessert.
|
||||
- Datei‑Picker auf `JFileChooser` umgestellt; editierbares Pfadfeld mit Validierung integriert.
|
||||
- Firewalld/mDNS‑Ursache für fehlende Sichtbarkeit zwischen Host/Client identifiziert und als ToDo/Guide dokumentiert.
|
||||
|
||||
## Changes
|
||||
- ConnectivityTracker: Fallback‑Kaskade readiness → health → /api/ping/simple; Intervalle angepasst; Debug‑Logs ergänzt.
|
||||
- main.kt: korrekte String‑Interpolation; Start‑Log der `NetworkConfig.baseUrl`; WS‑Port 8090 konsistent.
|
||||
- JmDnsDiscoveryService: Interface‑Filter (ohne docker/br/veth, private IPv4 priorisiert), Debounce/De‑Dup der Registrierung, Log‑Noise reduziert.
|
||||
- Navigation: Guard gegen Navigation auf gleichen Screen; Top‑Bar Tools erweitert (Reset/Backup/Settings‑Ordner öffnen).
|
||||
- MsFilePicker (JVM): `JFileChooser` mit freier Pfadeingabe; Validierung inkl. Schreib‑Probe; automatische Ordnererstellung bei Auswahl.
|
||||
- conveyor.conf: JVM‑Flag `--enable-native-access=ALL-UNNAMED` ergänzt (Netty‑Warnung mitigiert).
|
||||
|
||||
## Verification
|
||||
- Build (Gradle): erfolgreich ✓
|
||||
- Laufzeit/Netzwerk: Verifikation ausstehend (mDNS nach Firewall‑Freigaben; KDE‑Picker unter Fedora 44; Host/Client‑Sichtbarkeit LAN/WLAN) — Anti‑Halluzinations‑Protokoll beachtet.
|
||||
|
||||
## Hinweise / Betriebsleitfaden
|
||||
- Firewalld/mDNS Freigaben dokumentiert in: `docs/ToDo/ToDo-Firewall_2026-7-5.md` (mdns + Ports 8090/8080; Reload/Kontrolle; Avahi/Tcpdump Checks).
|
||||
|
||||
## Nächste Schritte
|
||||
1. KDE‑Directory‑Picker: auf `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus wechseln; präzisere Fehlermeldungen; HOME‑Fallback.
|
||||
2. Guard gegen mehrfachen P2P‑Start ergänzen.
|
||||
3. Conveyor/Windows‑Installer in CI (Runtime‑Flags; optional SLF4J‑Binding), danach erneute Laufzeit‑Verifikation.
|
||||
@@ -0,0 +1,72 @@
|
||||
🎨 [Frontend Expert]
|
||||
|
||||
### Kurzfazit
|
||||
- Die beiden Rechner sehen sich über mDNS höchstwahrscheinlich wegen Firewall‑Regeln/Zonen nicht. JmDNS registriert korrekt auf 192.168.0.124:8090, aber der Client (192.168.0.167, WLAN) empfängt keine mDNS‑Antworten → Fedora/Firewalld blockiert UDP 5353 Multicast in der aktiven Zone (WLAN/Public) sehr häufig standardmäßig.
|
||||
- Der Datei‑Picker unter Fedora KDE hakt im Verzeichnis‑Modus vermutlich wegen der Kombination DIRECTORIES_ONLY + SAVE_DIALOG und/oder wegen unserer „schreibbar“-Validierung.
|
||||
|
||||
### Sofortmaßnahmen (auf beiden Geräten, Host + Client)
|
||||
1) Firewalld freischalten (mdns + Ports) und neu laden
|
||||
```
|
||||
sudo firewall-cmd --get-active-zones
|
||||
# notieren, in welcher Zone das aktive Interface liegt (z. B. public, home)
|
||||
ZONE=public # ggf. anpassen
|
||||
|
||||
# mDNS erlauben (UDP 5353 Multicast)
|
||||
sudo firewall-cmd --zone=$ZONE --add-service=mdns --permanent
|
||||
|
||||
# unsere Dienste öffnen
|
||||
sudo firewall-cmd --zone=$ZONE --add-port=8090/tcp --permanent # WS-Chat
|
||||
sudo firewall-cmd --zone=$ZONE --add-port=8080/tcp --permanent # P2P-Server (PoC)
|
||||
|
||||
# Regeln anwenden
|
||||
sudo firewall-cmd --reload
|
||||
|
||||
# Kontrolle
|
||||
sudo firewall-cmd --zone=$ZONE --list-all
|
||||
```
|
||||
Hinweis: WLAN und Ethernet können in unterschiedlichen Zonen liegen (z. B. public vs. home). Bitte in beiden relevanten Zonen freischalten.
|
||||
|
||||
2) Optional: Avahi als Indikator prüfen (nicht zwingend für JmDNS, aber gut für mDNS‑Baseline)
|
||||
```
|
||||
systemctl status avahi-daemon
|
||||
```
|
||||
|
||||
3) mDNS‑Pakete verifizieren (nur wenn es nach 1) noch nicht klappt)
|
||||
```
|
||||
# Auf dem Client (WLAN‑Interface anpassen):
|
||||
sudo tcpdump -n -i wlp0s20f3 port 5353
|
||||
# Auf dem Host (Ethernet‑Interface anpassen):
|
||||
sudo tcpdump -n -i <eth-iface> port 5353
|
||||
```
|
||||
Erwartung: Bei App‑Start siehst du Multicast an 224.0.0.251. Wenn auf einer Seite „Stille“ herrscht, blockiert noch etwas.
|
||||
|
||||
4) WS‑Konnektivität direkt testen (um Networking unabhängig von mDNS zu prüfen)
|
||||
```
|
||||
# vom Client auf den Host
|
||||
websocat ws://192.168.0.124:8090/chat
|
||||
# oder wscat: wscat -c ws://192.168.0.124:8090/chat
|
||||
```
|
||||
Wenn das klappt, ist der WS‑Pfad ok; dann bleibt nur mDNS/Firewall als Ursache.
|
||||
|
||||
### Erklärung zu deinen Logs
|
||||
- „[Discovery] Dienst 'fedora-stefan' … (Port 8090)“ → Host hat korrekt via mDNS veröffentlicht.
|
||||
- „Discovery Update: 0 Dienste gefunden“ am Client → keine mDNS‑Antworten angekommen (typisches Firewall‑Symptom bei Fedora/KDE, vor allem in Zone „public“).
|
||||
- Mehrfach „[P2P Server] Gestartet auf Port 8080“ → Wir haben noch einen Trigger, der den P2P‑Start mehrfach feuert. Funktional unkritisch für den aktuellen Befund; ich bereinige das im nächsten Patch mit einem zusätzlichen Guard.
|
||||
|
||||
### Fedora KDE Datei‑Picker (JFileChooser) – Workarounds jetzt, Fix als Patch im Anschluss
|
||||
- Mögliche Ursache 1: `SAVE_DIALOG` im Ordner‑Modus unter KDE verhält sich zickig. Besser: `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus + eigener Bestätigungs‑Button‑Text.
|
||||
- Mögliche Ursache 2: Unsere Validierung „Ordner existiert und ist beschreibbar“ verhindert die Übernahme (z. B. externer Stick nur lesbar, NTFS ohne Schreibrechte, Netzwerkpfad ohne Rechte). Teste bitte einmal mit einem lokalen Pfad wie `~/MeldestelleBackup`.
|
||||
- Quick‑Test jetzt: Tippe im editierbaren Feld manuell `~/MeldestelleBackup`, bestätige → sollte grün werden. Falls rot: bitte den Text der Fehlermeldung nennen.
|
||||
|
||||
Geplanter Fix (nach deiner Bestätigung):
|
||||
- Directory‑Picker: Wechsel auf `showOpenDialog(null)` mit `JFileChooser.DIRECTORIES_ONLY`, `approveButtonText = "Auswählen"`, `acceptAllFileFilterUsed=false`. Fallback auf HOME, wenn `currentDirectory` ungültig. Beibehalt der freien Texteingabe.
|
||||
- Validierung: Fehlermeldung präziser („existiert nicht“ vs. „keine Schreibrechte“) und angebotene Auto‑Erstellung, wenn der User bestätigt.
|
||||
|
||||
### Bitte liefere kurz zurück
|
||||
- Nach Schritt 1 (firewalld): Finden sich Host (192.168.0.124) und Client (192.168.0.167) gegenseitig? Tauchen Services in der UI/Logs auf?
|
||||
- Falls noch nicht: kurzer Ausschnitt aus `tcpdump` beider Seiten (je 3–5 Zeilen genügen).
|
||||
- Datei‑Picker auf dem Client: Was passiert bei manuellem Pfad `~/MeldestelleBackup`? Erscheint eine Fehlermeldung? Wenn ja, welcher Text?
|
||||
|
||||
### Nächste Schritte (nach Feedback)
|
||||
- Ich liefere: Patch für den KDE‑Picker (OPEN_DIALOG) und einen zusätzlichen Guard gegen mehrfachen P2P‑Start; außerdem noch etwas Discovery‑Logging (Interface/Zonen‑Hinweis).
|
||||
- Danach kümmern wir uns um Conveyor (Windows‑Installer aus CI, inkl. JVM‑Flag gegen die Netty‑Warnung).
|
||||
+32
-2
@@ -19,12 +19,23 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
private val jmdnsInstances = mutableListOf<JmDNS>()
|
||||
private val SERVICE_TYPE = "_meldestelle._tcp.local."
|
||||
private val discoveredServicesMap = ConcurrentHashMap<String, DiscoveredService>()
|
||||
private val registeredSet = ConcurrentHashMap.newKeySet<String>() // key: "${name}@${addr.hostAddress}:$port"
|
||||
|
||||
// Debounce/Guards
|
||||
@Volatile private var lastStartRequestedAt: Long = 0L
|
||||
@Volatile private var lastStartIp: String? = null
|
||||
|
||||
private val _discoveredServices = MutableStateFlow<List<DiscoveredService>>(emptyList())
|
||||
override val discoveredServices: StateFlow<List<DiscoveredService>> = _discoveredServices.asStateFlow()
|
||||
|
||||
override fun startDiscovery(preferredIp: String?) {
|
||||
if (jmdnsInstances.isNotEmpty()) return
|
||||
// Debounce schnelle Folgeaufrufe mit identischer IP
|
||||
val now = System.currentTimeMillis()
|
||||
if (jmdnsInstances.isNotEmpty() && lastStartIp == preferredIp && (now - lastStartRequestedAt) < 500) {
|
||||
return
|
||||
}
|
||||
lastStartRequestedAt = now
|
||||
lastStartIp = preferredIp
|
||||
|
||||
val addresses = getRelevantAddresses(preferredIp)
|
||||
if (addresses.isEmpty()) {
|
||||
@@ -112,8 +123,13 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
)
|
||||
)
|
||||
try {
|
||||
val key = "${name}@${jmdns.inetAddress.hostAddress}:$port"
|
||||
if (registeredSet.add(key)) {
|
||||
jmdns.registerService(serviceInfo)
|
||||
println("[Discovery] Dienst '$name' auf ${jmdns.inetAddress} registriert (Port $port)")
|
||||
} else {
|
||||
// bereits registriert – kein Spam
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[Discovery] Fehler bei Registrierung auf ${jmdns.inetAddress}: ${e.message}")
|
||||
}
|
||||
@@ -130,13 +146,19 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val iface = interfaces.nextElement()
|
||||
val name = iface.name.lowercase()
|
||||
// Filtere Docker/Bridged/VETH/VM-Schnittstellen heraus
|
||||
if (iface.isLoopback || !iface.isUp || iface.isVirtual) continue
|
||||
if (name.startsWith("br-") || name.startsWith("docker") || name.startsWith("veth") || name.contains("vmnet") || name.contains("virbr")) continue
|
||||
|
||||
val inetAddresses = iface.inetAddresses
|
||||
while (inetAddresses.hasMoreElements()) {
|
||||
val addr = inetAddresses.nextElement()
|
||||
// Nur IPv4 für maximale Kompatibilität in lokalen Netzen (ÖTO/FEI Standardumgebungen)
|
||||
if (addr is java.net.Inet4Address) {
|
||||
// Exkludiere Link-Local
|
||||
val host = addr.hostAddress
|
||||
if (host.startsWith("169.254.")) continue
|
||||
addresses.add(addr)
|
||||
}
|
||||
}
|
||||
@@ -145,7 +167,15 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
println("[Discovery] Fehler beim Auflisten der Interfaces: ${e.message}")
|
||||
}
|
||||
|
||||
return if (addresses.isEmpty()) listOf(InetAddress.getLocalHost()) else addresses
|
||||
if (addresses.isEmpty()) return listOf(InetAddress.getLocalHost())
|
||||
|
||||
// Bevorzuge private LAN IPv4 (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||
fun isPrivateIPv4(a: InetAddress): Boolean {
|
||||
val h = a.hostAddress
|
||||
return h.startsWith("192.168.") || h.startsWith("10.") || (h.startsWith("172.") && h.split('.').getOrNull(1)?.toIntOrNull() in 16..31)
|
||||
}
|
||||
return addresses.sortedWith(compareByDescending<InetAddress> { isPrivateIPv4(it) }
|
||||
.thenBy { it.hostAddress })
|
||||
}
|
||||
|
||||
override fun getDiscoveredServices(): List<DiscoveredService> {
|
||||
|
||||
+3
-2
@@ -208,10 +208,11 @@ class DeviceInitializationViewModel(
|
||||
discoveryService.stopDiscovery()
|
||||
discoveryService.startDiscovery(ip)
|
||||
|
||||
// Falls wir ein Master sind, registrieren wir uns auch direkt, damit andere uns finden
|
||||
// Falls wir ein Master sind, starten wir den lokalen P2P‑Server.
|
||||
// Die mDNS‑Registrierung erfolgt zentral beim App‑Start (entkoppelt, um Duplikate zu vermeiden).
|
||||
if (uiState.value.settings.networkRole == NetworkRole.MASTER) {
|
||||
discoveryService.registerService(8080, ip, uiState.value.settings.deviceName)
|
||||
syncService.startServer(8080)
|
||||
println("[P2P Server] Gestartet auf Port 8080")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -214,7 +214,22 @@ actual fun DeviceInitializationConfig(
|
||||
MsFilePicker(
|
||||
label = "Backup-Verzeichnis (Plan-USB)",
|
||||
selectedPath = settings.backupPath,
|
||||
onFileSelected = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
|
||||
onFileSelected = { path ->
|
||||
if (path.isNotBlank()) {
|
||||
try {
|
||||
val dir = java.io.File(path)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val probe = java.io.File(dir, ".ms_write_test.tmp")
|
||||
probe.writeText("ok")
|
||||
probe.delete()
|
||||
viewModel.updateSettings { s -> s.copy(backupPath = path) }
|
||||
} catch (e: Exception) {
|
||||
println("[DeviceInit] Backup-Verzeichnis nicht beschreibbar: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
viewModel.updateSettings { s -> s.copy(backupPath = path) }
|
||||
}
|
||||
},
|
||||
directoryOnly = true,
|
||||
modifier = Modifier.focusRequester(backupPathFocus),
|
||||
enabled = !uiState.isLocked
|
||||
|
||||
+3
-3
@@ -73,13 +73,13 @@ fun main() = application {
|
||||
wsServer.start()
|
||||
val discovery = koin.get<NetworkDiscoveryService>()
|
||||
discovery.startDiscovery()
|
||||
// Im Host-Modus würden wir hier registerService aufrufen
|
||||
// Im Host-Modus würden wir hier registerService aufrufen.
|
||||
// Für den POC registrieren wir den lokalen Host-Dienst immer mit dem WS-Port
|
||||
try {
|
||||
discovery.registerService(wsServer.getPort())
|
||||
println("[DesktopApp] Discovery-Registrierung durchgeführt (Port ${wsServer.getPort()})")
|
||||
} catch (_: Exception) {
|
||||
println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${'$'}{e.message}")
|
||||
} catch (e: Exception) {
|
||||
println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${e.message}")
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
println("[DesktopApp] POC-Dienste konnten nicht gestartet werden: ${e.message}")
|
||||
|
||||
+6
-6
@@ -24,14 +24,14 @@ class DesktopNavigationPort : NavigationPort {
|
||||
}
|
||||
|
||||
override fun navigateToScreen(screen: AppScreen) {
|
||||
println("[DesktopNav] navigateToScreen -> $screen")
|
||||
// Aktuellen Screen auf den Stack legen, falls er nicht derselbe ist
|
||||
val current = _currentScreen.value
|
||||
if (current != screen) {
|
||||
backStack.add(current)
|
||||
// Begrenzung des Backstacks auf z. B. 50 Einträge
|
||||
if (backStack.size > 50) backStack.removeAt(0)
|
||||
if (current == screen) {
|
||||
// Keine Aktion/kein Log bei identischem Ziel – beruhigt die Navigation
|
||||
return
|
||||
}
|
||||
println("[DesktopNav] navigateToScreen -> $screen")
|
||||
backStack.add(current)
|
||||
if (backStack.size > 50) backStack.removeAt(0)
|
||||
_currentScreen.value = screen
|
||||
}
|
||||
|
||||
|
||||
+28
-7
@@ -151,24 +151,45 @@ fun DesktopTopHeader(
|
||||
try {
|
||||
val backupService: BackupService = GlobalContext.get().get<BackupService> { parametersOf(deviceName) }
|
||||
val result = backupService.exportDelta("poc-backup", backupPath, sharedKey)
|
||||
result.onSuccess { _ -> println($$"[Backup] Erfolgreich exportiert: $path") }
|
||||
.onFailure { _ -> println($$"[Backup] Fehler: ${e.message}") }
|
||||
} catch (_: Exception) {
|
||||
println($$"[Backup] Fehler bei der Initialisierung: ${e.message}")
|
||||
result.onSuccess { fileName -> println("[Backup] Erfolgreich exportiert: $fileName") }
|
||||
.onFailure { ex -> println("[Backup] Fehler: ${ex.message}") }
|
||||
} catch (e: Exception) {
|
||||
println("[Backup] Fehler bei der Initialisierung: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Einstellungen-Ordner öffnen") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
val settingsDir = DeviceInitializationSettingsManager.getSettingsFilePath()
|
||||
val parent = java.io.File(settingsDir).parentFile?.absolutePath ?: settingsDir
|
||||
try {
|
||||
// Versuche plattformspezifisch den Ordner zu öffnen
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
if (os.contains("win")) {
|
||||
Runtime.getRuntime().exec(arrayOf("explorer", parent))
|
||||
} else if (os.contains("mac")) {
|
||||
Runtime.getRuntime().exec(arrayOf("open", parent))
|
||||
} else {
|
||||
Runtime.getRuntime().exec(arrayOf("xdg-open", parent))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[Tools] Konnte Ordner nicht öffnen: ${e.message}. Pfad: $parent")
|
||||
}
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Einstellungen zurücksetzen") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
val res = DeviceInitializationSettingsManager.resetToFactoryDefaults(deleteDatabase = false)
|
||||
if (res.isSuccess) {
|
||||
println($$"[Reset] settings.json gelöscht: ${DeviceInitializationSettingsManager.getSettingsFilePath()}")
|
||||
println("[Reset] settings.json gelöscht: ${DeviceInitializationSettingsManager.getSettingsFilePath()}")
|
||||
} else {
|
||||
println($$"[Reset] Fehler: ${res.exceptionOrNull()?.message}")
|
||||
println("[Reset] Fehler: ${res.exceptionOrNull()?.message}")
|
||||
}
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
}
|
||||
@@ -181,7 +202,7 @@ fun DesktopTopHeader(
|
||||
if (res.isSuccess) {
|
||||
println("[Reset] settings + ~/.meldestelle gelöscht")
|
||||
} else {
|
||||
println("[Reset] Fehler: ${'$'}{res.exceptionOrNull()?.message}")
|
||||
println("[Reset] Fehler: ${res.exceptionOrNull()?.message}")
|
||||
}
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user