2 Commits

Author SHA1 Message Date
eb218c33ca Fix missing key.reset() in BackendSync directory watch logic
All checks were successful
SteamWarCI Build successful
2025-07-20 00:08:51 +02:00
7b01f11b5b Introduce backend synchronization system for Velocity and WebsiteBackend
All checks were successful
SteamWarCI Build successful
2025-07-20 00:06:19 +02:00
6 changed files with 187 additions and 0 deletions

View File

@ -0,0 +1,25 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.data;
public class SyncCommands {
public static final String RELOAD_PLAYER = "reload_player";
public static final String RELOAD_EVENT = "reload_event";
}

View File

@ -0,0 +1,97 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore;
import de.steamwar.data.SyncCommands;
import de.steamwar.sql.Event;
import de.steamwar.sql.EventFight;
import de.steamwar.sql.SteamwarUser;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
public class BackendSync implements Runnable {
private static final Path syncPath = new File("/run/sync").toPath();
private final Thread thread;
private final WatchService watchService;
public BackendSync() {
try {
watchService
= FileSystems.getDefault().newWatchService();
} catch (IOException e) {
throw new SecurityException("Could not create watch service", e);
}
thread = new Thread(this, "BackendSync");
thread.start();
}
public void stop() {
try {
watchService.close();
thread.join();
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void handleCommand(String name) {
try {
String[] parts = name.split("\\.");
switch (parts[0]) {
case SyncCommands.RELOAD_EVENT -> EventFight.loadAllComingFights();
case SyncCommands.RELOAD_PLAYER -> SteamwarUser.invalidate(Integer.parseInt(parts[1]));
default -> VelocityCore.getLogger().warning("Unknown command: " + name);
}
} catch (Exception e) {
VelocityCore.getLogger().throwing(this.getClass().getName(), "handleCommand", e);
}
}
@Override
public void run() {
try {
syncPath.register(watchService,
java.nio.file.StandardWatchEventKinds.ENTRY_CREATE);
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
String command = event.context().toString();
handleCommand(command);
if (!VelocityCore.get().getConfig().isEventmode()) {
Path path = syncPath.resolve((Path) event.context());
Files.delete(path);
}
}
key.reset();
}
} catch (InterruptedException | ClosedWatchServiceException ignored) {
Thread.currentThread().interrupt();
} catch (Exception e) {
VelocityCore.getLogger().throwing(this.getClass().getName(), "run", e);
}
}
}

View File

@ -95,6 +95,7 @@ public class VelocityCore implements ReloadablePlugin {
private Config config; private Config config;
private ErrorLogger errorLogger; private ErrorLogger errorLogger;
private TablistManager tablistManager; private TablistManager tablistManager;
private BackendSync backendSync;
@Getter @Getter
private TeamCommand teamCommand; private TeamCommand teamCommand;
@ -147,6 +148,8 @@ public class VelocityCore implements ReloadablePlugin {
new ReplayMod(); new ReplayMod();
new FML2(); new FML2();
backendSync = new BackendSync();
new ConnectionListener(); new ConnectionListener();
new ChatListener(); new ChatListener();
new BanListener(); new BanListener();
@ -268,6 +271,8 @@ public class VelocityCore implements ReloadablePlugin {
logger.log(Level.SEVERE, "Could not shutdown discord bot", e); logger.log(Level.SEVERE, "Could not shutdown discord bot", e);
} }
backendSync.stop();
if(tablistManager != null) if(tablistManager != null)
tablistManager.disable(); tablistManager.disable();
errorLogger.unregister(); errorLogger.unregister();

View File

@ -0,0 +1,49 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.data
import de.steamwar.sql.SteamwarUser
import java.io.File
import java.nio.file.Files
object VelocitySync {
private const val SYNC_PATH = "/run/sync"
private val SYNC_FILE = File(SYNC_PATH)
private val isWindows = System.getProperty("os.name").lowercase().contains("win")
private val lastSendMap = mutableMapOf<String, Long>()
private fun sendCommand(command: String, vararg args: String) {
if (isWindows) { return }
val name = command + if (args.isNotEmpty()) "." + args.joinToString(".") else ""
if (lastSendMap[name] != null || lastSendMap[name]!! > System.currentTimeMillis() - 1000) {
return
}
lastSendMap[name] = System.currentTimeMillis()
Files.createFile(File(SYNC_FILE, name).toPath())
}
fun reloadEvent() = sendCommand(SyncCommands.RELOAD_EVENT)
fun reloadPlayer(user: SteamwarUser) = sendCommand(SyncCommands.RELOAD_PLAYER, user.id.toString())
}

View File

@ -20,6 +20,7 @@
package de.steamwar.routes package de.steamwar.routes
import de.steamwar.ResponseError import de.steamwar.ResponseError
import de.steamwar.data.VelocitySync
import de.steamwar.sql.* import de.steamwar.sql.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
@ -112,6 +113,7 @@ fun Route.configureEventFightRoutes() {
if (fight.group != null) { if (fight.group != null) {
eventFight.setGroup(fight.group) eventFight.setGroup(fight.group)
} }
VelocitySync.reloadEvent()
call.respond(HttpStatusCode.Created, ResponseEventFight(eventFight)) call.respond(HttpStatusCode.Created, ResponseEventFight(eventFight))
} }
route("/{fight}") { route("/{fight}") {
@ -143,11 +145,13 @@ fun Route.configureEventFightRoutes() {
} }
fight.update(start, spielmodus, map, teamBlue, teamRed, spectatePort) fight.update(start, spielmodus, map, teamBlue, teamRed, spectatePort)
VelocitySync.reloadEvent()
call.respond(HttpStatusCode.OK, ResponseEventFight(fight)) call.respond(HttpStatusCode.OK, ResponseEventFight(fight))
} }
delete { delete {
val fight = call.receiveFight() ?: return@delete val fight = call.receiveFight() ?: return@delete
fight.delete() fight.delete()
VelocitySync.reloadEvent()
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} }

View File

@ -19,6 +19,7 @@
package de.steamwar.routes package de.steamwar.routes
import de.steamwar.data.VelocitySync
import de.steamwar.plugins.SWPermissionCheck import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.plugins.getUser import de.steamwar.plugins.getUser
import de.steamwar.sql.SteamwarUser import de.steamwar.sql.SteamwarUser
@ -86,6 +87,8 @@ fun Route.configureUserPerms() {
} }
UserPerm.addPerm(user, UserPerm.entries.find { it == prefix }!!) UserPerm.addPerm(user, UserPerm.entries.find { it == prefix }!!)
VelocitySync.reloadPlayer(user)
call.respond(HttpStatusCode.Accepted) call.respond(HttpStatusCode.Accepted)
} }
put("/{perm}") { put("/{perm}") {
@ -96,6 +99,8 @@ fun Route.configureUserPerms() {
call.respond(HttpStatusCode.Accepted) call.respond(HttpStatusCode.Accepted)
return@put return@put
} }
VelocitySync.reloadPlayer(user)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
delete("/{perm}") { delete("/{perm}") {
@ -106,6 +111,8 @@ fun Route.configureUserPerms() {
call.respond(HttpStatusCode.Accepted) call.respond(HttpStatusCode.Accepted)
return@delete return@delete
} }
VelocitySync.reloadPlayer(user)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
} }