Compare commits
3 Commits
3aaf5cc59c
...
74ef6424b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 74ef6424b7 | |||
| 3959168695 | |||
| 04a435df1d |
@@ -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.
|
||||||
+30
-12
@@ -33,11 +33,21 @@ actual fun MsFilePicker(
|
|||||||
else {
|
else {
|
||||||
val f = File(currentValue)
|
val f = File(currentValue)
|
||||||
if (directoryOnly) {
|
if (directoryOnly) {
|
||||||
val ok = f.exists() && f.isDirectory && f.canWrite()
|
when {
|
||||||
(!ok) to if (!ok) "Ordner existiert nicht oder ist nicht beschreibbar" else null
|
!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 {
|
} else {
|
||||||
val ok = (f.exists() && f.isFile && f.canWrite()) || (f.parentFile?.canWrite() == true)
|
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 {
|
SwingUtilities.invokeLater {
|
||||||
val chooser = JFileChooser().apply {
|
val chooser = JFileChooser().apply {
|
||||||
isMultiSelectionEnabled = false
|
isMultiSelectionEnabled = false
|
||||||
|
isAcceptAllFileFilterUsed = false
|
||||||
|
approveButtonText = "Auswählen"
|
||||||
|
|
||||||
// Initiales Verzeichnis/Pfad
|
// Initiales Verzeichnis/Pfad
|
||||||
selectedPath?.let { p ->
|
run {
|
||||||
val f = File(p)
|
val home = File(System.getProperty("user.home") ?: ".")
|
||||||
currentDirectory = when {
|
val initial = selectedPath?.takeIf { it.isNotBlank() }?.let { File(it) }
|
||||||
f.isDirectory -> f
|
val baseDir = when {
|
||||||
f.parentFile?.isDirectory == true -> f.parentFile
|
initial == null -> home
|
||||||
else -> currentDirectory
|
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) {
|
if (directoryOnly) {
|
||||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
dialogType = JFileChooser.SAVE_DIALOG // ermöglicht "Neuer Ordner"
|
// KDE/Plasma: OPEN_DIALOG im DIRECTORIES_ONLY‑Modus verwenden (kein Save‑Dialog)
|
||||||
|
dialogType = JFileChooser.OPEN_DIALOG
|
||||||
} else {
|
} else {
|
||||||
fileSelectionMode = JFileChooser.FILES_ONLY
|
fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
if (fileExtensions.isNotEmpty()) {
|
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) {
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
val chosen = chooser.selectedFile
|
val chosen = chooser.selectedFile
|
||||||
if (directoryOnly) {
|
if (directoryOnly) {
|
||||||
|
|||||||
@@ -51,5 +51,10 @@ kotlin {
|
|||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commonTest.dependencies {
|
||||||
|
implementation(libs.kotlin.test)
|
||||||
|
implementation(libs.kotlinx.coroutines.test)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-26
@@ -15,9 +15,15 @@ import kotlinx.coroutines.flow.*
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class JvmP2pSyncService : P2pSyncService {
|
class JvmP2pSyncService : P2pSyncService {
|
||||||
|
companion object {
|
||||||
|
// Prozessweiter, portbasierter Guard gegen Mehrfachstart
|
||||||
|
private val startedPorts: MutableSet<Int> = ConcurrentHashMap.newKeySet()
|
||||||
|
}
|
||||||
private var server: EmbeddedServer<*, *>? = null
|
private var server: EmbeddedServer<*, *>? = null
|
||||||
|
private var currentPort: Int? = null
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
install(io.ktor.client.plugins.websocket.WebSockets)
|
install(io.ktor.client.plugins.websocket.WebSockets)
|
||||||
}
|
}
|
||||||
@@ -32,41 +38,66 @@ class JvmP2pSyncService : P2pSyncService {
|
|||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
override fun startServer(port: Int) {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
server = embeddedServer(Netty, port = port) {
|
// Prozessweiter, portbasierter Guard
|
||||||
install(io.ktor.server.websocket.WebSockets)
|
if (!startedPorts.add(port)) {
|
||||||
routing {
|
println("[P2P Server] Bereits gestartet (Prozess) auf Port $port – idempotent, kein neuer Bind")
|
||||||
webSocket("/sync") {
|
return
|
||||||
println("[P2P Server] Neuer Peer verbunden")
|
}
|
||||||
activeSessions.add(this)
|
|
||||||
updatePeers()
|
try {
|
||||||
try {
|
server = embeddedServer(Netty, port = port) {
|
||||||
for (frame in incoming) {
|
install(io.ktor.server.websocket.WebSockets)
|
||||||
if (frame is Frame.Text) {
|
routing {
|
||||||
val text = frame.readText()
|
webSocket("/sync") {
|
||||||
try {
|
println("[P2P Server] Neuer Peer verbunden")
|
||||||
val event = Json.decodeFromString<SyncEvent>(text)
|
activeSessions.add(this)
|
||||||
_incomingEvents.emit(event)
|
updatePeers()
|
||||||
} catch (e: Exception) {
|
try {
|
||||||
println("[P2P Server] Fehler beim Dekodieren: ${e.message}")
|
for (frame in incoming) {
|
||||||
|
if (frame is Frame.Text) {
|
||||||
|
val text = frame.readText()
|
||||||
|
try {
|
||||||
|
val event = Json.decodeFromString<SyncEvent>(text)
|
||||||
|
_incomingEvents.emit(event)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("[P2P Server] Fehler beim Dekodieren: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
activeSessions.remove(this)
|
||||||
|
updatePeers()
|
||||||
|
println("[P2P Server] Peer getrennt")
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
activeSessions.remove(this)
|
|
||||||
updatePeers()
|
|
||||||
println("[P2P Server] Peer getrennt")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.start(wait = false)
|
||||||
}.start(wait = false)
|
currentPort = port
|
||||||
println("[P2P Server] Gestartet auf Port $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() {
|
override fun stopServer() {
|
||||||
server?.stop(1000, 2000)
|
try {
|
||||||
server = null
|
server?.stop(1000, 2000)
|
||||||
|
} finally {
|
||||||
|
server = null
|
||||||
|
currentPort?.let { startedPorts.remove(it) }
|
||||||
|
currentPort = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun connectToPeer(host: String, port: Int) {
|
override suspend fun connectToPeer(host: String, port: Int) {
|
||||||
|
|||||||
+37
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user