From fbe4d786ca120039a825c14d2a35cadbb041f43c Mon Sep 17 00:00:00 2001 From: Dmitri Shimanski Date: Mon, 20 Jul 2026 04:16:07 +0300 Subject: [PATCH] Make plugin manager lifecycle and inspection APIs thread-safe --- .../host/GoPluginManager.java | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/main/java/dev/yawaflua/gominecraftbridge/host/GoPluginManager.java b/src/main/java/dev/yawaflua/gominecraftbridge/host/GoPluginManager.java index d8e6459..7a9b443 100644 --- a/src/main/java/dev/yawaflua/gominecraftbridge/host/GoPluginManager.java +++ b/src/main/java/dev/yawaflua/gominecraftbridge/host/GoPluginManager.java @@ -46,7 +46,8 @@ public final class GoPluginManager { private final ActionExecutor actions = new ActionExecutor(); private final Map plugins = new LinkedHashMap<>(); private final List packageInspections = new ArrayList<>(); - private boolean serverRunning; + /** Lifecycle flag is read by the networking/admin thread as well as the server thread. */ + private volatile boolean serverRunning; public GoPluginManager(Logger logger) { this.logger = logger; @@ -55,7 +56,18 @@ public final class GoPluginManager { this.dataDirectory = root.resolve("data"); } - public void discover() { + public synchronized void discover() { + scanCandidates(null); + } + + public synchronized ReloadResult rescan(MinecraftServer server) { + int before = this.plugins.size(); + scanCandidates(server); + int added = this.plugins.size() - before; + return new ReloadResult(true, "Package check completed; new plugins: " + added); + } + + private void scanCandidates(MinecraftServer server) { this.packageInspections.clear(); try { Files.createDirectories(this.pluginDirectory); @@ -66,6 +78,18 @@ public final class GoPluginManager { for (Path candidate : nativeCandidates()) { try { + Path normalized = candidate.toAbsolutePath().normalize(); + LoadedPlugin alreadyLoaded = this.plugins.values().stream() + .filter(plugin -> plugin.backend().origin().equals(normalized)) + .findFirst() + .orElse(null); + if (alreadyLoaded != null) { + this.packageInspections.add(new PackageInspection( + normalized.toString(), true, alreadyLoaded.metadata().id(), null + )); + continue; + } + LoadedPlugin plugin = new LoadedPlugin(new NativePluginBackend(candidate)); LoadedPlugin duplicate = this.plugins.putIfAbsent(plugin.metadata().id(), plugin); if (duplicate != null) { @@ -85,6 +109,9 @@ public final class GoPluginManager { plugin.metadata().version(), candidate.getFileName() ); + if (server != null && this.serverRunning) { + startPlugin(plugin, server); + } } catch (RuntimeException exception) { this.packageInspections.add(new PackageInspection( candidate.toAbsolutePath().normalize().toString(), @@ -97,33 +124,33 @@ public final class GoPluginManager { } } - public void start(MinecraftServer server) { + public synchronized void start(MinecraftServer server) { this.serverRunning = true; for (LoadedPlugin plugin : this.plugins.values()) { startPlugin(plugin, server); } } - public void tick(MinecraftServer server) { + public synchronized void tick(MinecraftServer server) { for (LoadedPlugin plugin : runningPlugins()) { ServerSnapshot snapshot = this.snapshots.create(server, plugin.snapshotSubscription()); invoke(plugin, Protocol.Operation.TICK, snapshot, server); } } - public void chat(ChatEvent event, MinecraftServer server) { + public synchronized void chat(ChatEvent event, MinecraftServer server) { for (LoadedPlugin plugin : runningPlugins()) { invoke(plugin, Protocol.Operation.CHAT, event, server); } } - public void death(DeathEvent event, MinecraftServer server) { + public synchronized void death(DeathEvent event, MinecraftServer server) { for (LoadedPlugin plugin : runningPlugins()) { invoke(plugin, Protocol.Operation.DEATH, event, server); } } - public void stop(MinecraftServer server) { + public synchronized void stop(MinecraftServer server) { for (LoadedPlugin plugin : runningPlugins()) { try { PluginResponse response = plugin.invoke( @@ -141,10 +168,21 @@ public final class GoPluginManager { this.serverRunning = false; } - public List plugins() { + public synchronized List plugins() { return List.copyOf(this.plugins.values()); } + /** Returns the latest package validation results as an immutable snapshot. */ + public synchronized List packageInspections() { + return List.copyOf(this.packageInspections); + } + + /** Returns a point-in-time copy of a plugin's retained logs, or an empty list when unknown. */ + public synchronized List pluginLogs(String pluginId) { + LoadedPlugin plugin = this.plugins.get(pluginId); + return plugin == null ? List.of() : plugin.logs(); + } + public synchronized BridgeManagementSnapshot managementSnapshot(boolean canReload, String message) { List pluginSnapshots = this.plugins.values().stream() .map(plugin -> new ManagedPluginSnapshot(