Make plugin manager lifecycle and inspection APIs thread-safe

This commit is contained in:
Dmitri Shimanski
2026-07-20 04:16:07 +03:00
parent 020da28470
commit fbe4d786ca
@@ -46,7 +46,8 @@ public final class GoPluginManager {
private final ActionExecutor actions = new ActionExecutor(); private final ActionExecutor actions = new ActionExecutor();
private final Map<String, LoadedPlugin> plugins = new LinkedHashMap<>(); private final Map<String, LoadedPlugin> plugins = new LinkedHashMap<>();
private final List<PackageInspection> packageInspections = new ArrayList<>(); private final List<PackageInspection> 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) { public GoPluginManager(Logger logger) {
this.logger = logger; this.logger = logger;
@@ -55,7 +56,18 @@ public final class GoPluginManager {
this.dataDirectory = root.resolve("data"); 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(); this.packageInspections.clear();
try { try {
Files.createDirectories(this.pluginDirectory); Files.createDirectories(this.pluginDirectory);
@@ -66,6 +78,18 @@ public final class GoPluginManager {
for (Path candidate : nativeCandidates()) { for (Path candidate : nativeCandidates()) {
try { 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 plugin = new LoadedPlugin(new NativePluginBackend(candidate));
LoadedPlugin duplicate = this.plugins.putIfAbsent(plugin.metadata().id(), plugin); LoadedPlugin duplicate = this.plugins.putIfAbsent(plugin.metadata().id(), plugin);
if (duplicate != null) { if (duplicate != null) {
@@ -85,6 +109,9 @@ public final class GoPluginManager {
plugin.metadata().version(), plugin.metadata().version(),
candidate.getFileName() candidate.getFileName()
); );
if (server != null && this.serverRunning) {
startPlugin(plugin, server);
}
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
this.packageInspections.add(new PackageInspection( this.packageInspections.add(new PackageInspection(
candidate.toAbsolutePath().normalize().toString(), 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; this.serverRunning = true;
for (LoadedPlugin plugin : this.plugins.values()) { for (LoadedPlugin plugin : this.plugins.values()) {
startPlugin(plugin, server); startPlugin(plugin, server);
} }
} }
public void tick(MinecraftServer server) { public synchronized void tick(MinecraftServer server) {
for (LoadedPlugin plugin : runningPlugins()) { for (LoadedPlugin plugin : runningPlugins()) {
ServerSnapshot snapshot = this.snapshots.create(server, plugin.snapshotSubscription()); ServerSnapshot snapshot = this.snapshots.create(server, plugin.snapshotSubscription());
invoke(plugin, Protocol.Operation.TICK, snapshot, server); 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()) { for (LoadedPlugin plugin : runningPlugins()) {
invoke(plugin, Protocol.Operation.CHAT, event, server); 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()) { for (LoadedPlugin plugin : runningPlugins()) {
invoke(plugin, Protocol.Operation.DEATH, event, server); invoke(plugin, Protocol.Operation.DEATH, event, server);
} }
} }
public void stop(MinecraftServer server) { public synchronized void stop(MinecraftServer server) {
for (LoadedPlugin plugin : runningPlugins()) { for (LoadedPlugin plugin : runningPlugins()) {
try { try {
PluginResponse response = plugin.invoke( PluginResponse response = plugin.invoke(
@@ -141,10 +168,21 @@ public final class GoPluginManager {
this.serverRunning = false; this.serverRunning = false;
} }
public List<LoadedPlugin> plugins() { public synchronized List<LoadedPlugin> plugins() {
return List.copyOf(this.plugins.values()); return List.copyOf(this.plugins.values());
} }
/** Returns the latest package validation results as an immutable snapshot. */
public synchronized List<PackageInspection> 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<PluginLog> pluginLogs(String pluginId) {
LoadedPlugin plugin = this.plugins.get(pluginId);
return plugin == null ? List.of() : plugin.logs();
}
public synchronized BridgeManagementSnapshot managementSnapshot(boolean canReload, String message) { public synchronized BridgeManagementSnapshot managementSnapshot(boolean canReload, String message) {
List<ManagedPluginSnapshot> pluginSnapshots = this.plugins.values().stream() List<ManagedPluginSnapshot> pluginSnapshots = this.plugins.values().stream()
.map(plugin -> new ManagedPluginSnapshot( .map(plugin -> new ManagedPluginSnapshot(