3 Commits

5 changed files with 162 additions and 38 deletions
@@ -0,0 +1,33 @@
---
type: Journal
status: COMPLETED
owner: Curator
last_update: 2026-05-08
---
# 2026-05-08 — Session Log (P2P Guards, FilePicker & Test Verification)
## Kontext
- Fokus: Stabilisierung des P2P-Sync-Servers (Guard gegen Mehrfachstart) und finale Optimierung des JVM File-Pickers für KDE/Fedora.
- Basierend auf den ToDos vom Vortag.
## Summary
- **P2P Sync Guard:** `JvmP2pSyncService` wurde um einen port-basierten Guard erweitert. Mehrfache Start-Aufrufe auf demselben Port werden nun prozessweit abgefangen (idempotent), was Ressourcen schont und Fehler beim Bind verhindert.
- **Test-Verifikation:** Neuer Integration-Test `JvmP2pSyncServiceTest` erstellt, der das Guard-Verhalten und die Freigabe des Ports nach Stop verifiziert.
- **MsFilePicker (JVM):** Finale Anpassungen für KDE (Fedora 44). Umstellung auf `isAcceptAllFileFilterUsed = false` und explizites `approveButtonText = "Auswählen"`. Der Directory-Picker nutzt nun konsequent `OPEN_DIALOG` im `DIRECTORIES_ONLY` Modus.
- **Build-Fix:** Ein Tippfehler (`acceptAllFileFilterUsed` -> `isAcceptAllFileFilterUsed`) wurde korrigiert.
## Changes
- `at.mocode.frontend.core.network.sync.JvmP2pSyncService`: Port-Guard integriert.
- `at.mocode.frontend.core.network.sync.JvmP2pSyncServiceTest`: Neuer JVM-Test (verifiziert ✅).
- `at.mocode.frontend.core.designsystem.components.MsFilePicker.jvm.kt`: UI-Anpassungen für Swing JFileChooser.
- `frontend/core/network/build.gradle.kts`: Test-Abhängigkeiten hinzugefügt.
## Verification
- **Unit/Integration Tests:** `JvmP2pSyncServiceTest` erfolgreich durchgelaufen ✓.
- **Build (Gradle):** Gesamter Build inkl. Packaging-Hüllen erfolgreich ✓.
- **Laufzeit (Netzwerk):** P2P-Guard loggt korrekt: "[P2P Server] Bereits gestartet...". Discovery-Sichtbarkeit LAN/WLAN weiterhin abhängig von Firewalld-Status (siehe ToDo-Firewall).
## Nächste Schritte
1. Conveyor-Build auf einem x86_64 Runner (oder lokal) verifizieren, um Windows-Installer zu erzeugen.
2. Erste physische Turnier-Hierarchie (MEILENSTEIN 1) angehen.
@@ -33,11 +33,21 @@ actual fun MsFilePicker(
else {
val f = File(currentValue)
if (directoryOnly) {
val ok = f.exists() && f.isDirectory && f.canWrite()
(!ok) to if (!ok) "Ordner existiert nicht oder ist nicht beschreibbar" else null
when {
!f.exists() -> true to "Ordner existiert nicht"
!f.isDirectory -> true to "Pfad ist kein Ordner"
!f.canWrite() -> true to "Ordner ist schreibgeschützt"
else -> false to null
}
} else {
val ok = (f.exists() && f.isFile && f.canWrite()) || (f.parentFile?.canWrite() == true)
(!ok) to if (!ok) "Datei/Ordner nicht beschreibbar" else null
(!ok) to if (!ok) {
when {
!f.exists() && f.parentFile?.exists() != true -> "Pfad existiert nicht"
f.exists() && !f.isFile -> "Pfad ist keine Datei"
else -> "Datei/Ordner nicht beschreibbar"
}
} else null
}
}
}
@@ -68,20 +78,28 @@ actual fun MsFilePicker(
SwingUtilities.invokeLater {
val chooser = JFileChooser().apply {
isMultiSelectionEnabled = false
isAcceptAllFileFilterUsed = false
approveButtonText = "Auswählen"
// Initiales Verzeichnis/Pfad
selectedPath?.let { p ->
val f = File(p)
currentDirectory = when {
f.isDirectory -> f
f.parentFile?.isDirectory == true -> f.parentFile
else -> currentDirectory
run {
val home = File(System.getProperty("user.home") ?: ".")
val initial = selectedPath?.takeIf { it.isNotBlank() }?.let { File(it) }
val baseDir = when {
initial == null -> home
directoryOnly && initial.isDirectory -> initial
!directoryOnly && initial.isFile -> initial.parentFile ?: home
initial.parentFile?.isDirectory == true -> initial.parentFile
else -> home
}
if (!directoryOnly && f.isFile) selectedFile = f
currentDirectory = baseDir
if (!directoryOnly && initial?.isFile == true) selectedFile = initial
}
if (directoryOnly) {
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
dialogType = JFileChooser.SAVE_DIALOG // ermöglicht "Neuer Ordner"
// KDE/Plasma: OPEN_DIALOG im DIRECTORIES_ONLYModus verwenden (kein SaveDialog)
dialogType = JFileChooser.OPEN_DIALOG
} else {
fileSelectionMode = JFileChooser.FILES_ONLY
if (fileExtensions.isNotEmpty()) {
@@ -93,7 +111,7 @@ actual fun MsFilePicker(
}
}
val result = chooser.showDialog(null, "Auswählen")
val result = chooser.showOpenDialog(null)
if (result == JFileChooser.APPROVE_OPTION) {
val chosen = chooser.selectedFile
if (directoryOnly) {
+5
View File
@@ -51,5 +51,10 @@ kotlin {
implementation(libs.ktor.client.js)
implementation(libs.kotlinx.coroutines.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
}
@@ -15,9 +15,15 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class JvmP2pSyncService : P2pSyncService {
companion object {
// Prozessweiter, portbasierter Guard gegen Mehrfachstart
private val startedPorts: MutableSet<Int> = ConcurrentHashMap.newKeySet()
}
private var server: EmbeddedServer<*, *>? = null
private var currentPort: Int? = null
private val client = HttpClient {
install(io.ktor.client.plugins.websocket.WebSockets)
}
@@ -32,8 +38,19 @@ class JvmP2pSyncService : P2pSyncService {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun startServer(port: Int) {
if (server != null) return
// Instanz-Guard (gleiche Instanz)
if (server != null) {
println("[P2P Server] Bereits gestartet (Instanz) auf Port ${currentPort ?: port} idempotent")
return
}
// Prozessweiter, portbasierter Guard
if (!startedPorts.add(port)) {
println("[P2P Server] Bereits gestartet (Prozess) auf Port $port idempotent, kein neuer Bind")
return
}
try {
server = embeddedServer(Netty, port = port) {
install(io.ktor.server.websocket.WebSockets)
routing {
@@ -61,12 +78,26 @@ class JvmP2pSyncService : P2pSyncService {
}
}
}.start(wait = false)
currentPort = port
println("[P2P Server] Gestartet auf Port $port")
} catch (e: Exception) {
// Start fehlgeschlagen -> Port-Lock wieder freigeben
startedPorts.remove(port)
server = null
currentPort = null
println("[P2P Server] Start auf Port $port fehlgeschlagen: ${e.message}")
throw e
}
}
override fun stopServer() {
try {
server?.stop(1000, 2000)
} finally {
server = null
currentPort?.let { startedPorts.remove(it) }
currentPort = null
}
}
override suspend fun connectToPeer(host: String, port: Int) {
@@ -0,0 +1,37 @@
package at.mocode.frontend.core.network.sync
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class JvmP2pSyncServiceTest {
@Test
fun starting_server_twice_on_same_port_should_not_fail_but_use_guard() = runTest {
val service1 = JvmP2pSyncService()
val service2 = JvmP2pSyncService()
val port = 9091
try {
service1.startServer(port)
// Second start should just return/log and not throw an exception (idempotent)
service2.startServer(port)
} finally {
service1.stopServer()
service2.stopServer()
}
}
@Test
fun stopping_server_should_release_port_lock() = runTest {
val service1 = JvmP2pSyncService()
val service2 = JvmP2pSyncService()
val port = 9092
service1.startServer(port)
service1.stopServer()
// After stopping, starting again on same port (even from different instance) should work
service2.startServer(port)
service2.stopServer()
}
}