Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/maven-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ jobs:
"xin.bbtt.mcbot.listeners.CommandsRecorderTest",
"xin.bbtt.mcbot.plugin.PluginClassloaderLeakTest",
"xin.bbtt.mcbot.config.BotConfigDataTest",
"xin.bbtt.mcbot.plugin.PluginYmlValidationTest"
"xin.bbtt.mcbot.plugin.PluginYmlValidationTest",
"xin.bbtt.mcbot.LoginFlow.LoginFlowTest",
"xin.bbtt.mcbot.LoginFlow.LoginFlowRealWorldTest",
"xin.bbtt.mcbot.modpack.ModpackTest",
"xin.bbtt.mcbot.plugin.MetaPluginDependencyTest",
"xin.bbtt.mcbot.plugin.PluginRuntimeSoftDependTest"
]

steps:
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<groupId>xin.bbtt</groupId>
<artifactId>xinbot</artifactId>
<version>2.3.1-RELEASE</version>
<version>2.3.2-SNAPSHOT</version>
<packaging>jar</packaging>

<repositories>
Expand Down
73 changes: 52 additions & 21 deletions src/main/java/xin/bbtt/mcbot/plugin/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,28 +138,20 @@ public void loadPlugin(File pluginFile) throws Exception {

if (plugins.containsKey(info.name)) return;

PluginClassLoader pluginClassLoader = new PluginClassLoader(new URL[]{url}, PluginManager.class.getClassLoader());
pluginLoaders.put(info.name, pluginClassLoader);
pluginDependencies.put(info.name, info.depends);

Plugin plugin;
try {
Class<?> clazz = Class.forName(info.mainClass, true, pluginClassLoader);
plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
pluginLoaders.remove(info.name);
pluginDependencies.remove(info.name);
try { pluginClassLoader.close(); } catch (IOException ignored) {}
throw e;
}

RegisteredPlugin rp;
if (info.type == PluginType.META_PLUGIN && plugin instanceof MetaPlugin) {
rp = new RegisteredMetaPlugin(info.name, info.version, info.mainClass, info.depends, info.file, url, (MetaPlugin) plugin);
} else {
rp = new RegisteredPlugin(info.name, info.version, info.mainClass, info.depends, info.file, url, plugin, PluginType.PLUGIN);
// The startup batch loader refuses plugins with missing hard dependencies
// (topological sort excludes them); the runtime path must do the same instead
// of silently loading with a broken classloader chain.
for (String dep : info.depends) {
if (!plugins.containsKey(dep)) {
throw new IllegalArgumentException(LangManager.get("xinbot.plugin.dependency.missing", dep, info.name));
}
}
loadPlugin(rp);

info.file = pluginFile;
info.url = url;
// Delegate to the shared loader so runtime loads (e.g. `pm load`/`pm reload`)
// wire up the (soft)dependency classloader chain just like startup batch loading.
instantiateAndLoad(info);
}

public void loadPlugins(String pluginsDirectory) {
Expand Down Expand Up @@ -449,6 +441,10 @@ public void unloadPlugin(RegisteredPlugin rp) {
return;
}
if (!plugins.containsKey(rp.getName())) return;
if (Bot.INSTANCE.isRunning() && isMetaPluginDependency(rp.getName())) {
log.error(LangManager.get("xinbot.metaplugin.error.unload_dependency_runtime", rp.getName()));
return;
}

String pluginName = rp.getName();
unloadDependents(pluginName);
Expand All @@ -475,6 +471,36 @@ private void unloadDependents(String pluginName) {
}
}

// A plugin the meta plugin (transitively) depends on shares the meta plugin's
// lifecycle restrictions: unloading it would tear down the meta plugin's
// classloader chain, so it must not be unloaded during runtime either.
private boolean isMetaPluginDependency(String pluginName) {
for (RegisteredPlugin p : plugins.values()) {
if (p instanceof RegisteredMetaPlugin
&& getTransitiveDependencies(p.getName()).contains(pluginName)) {
return true;
}
}
return false;
}

private List<String> getTransitiveDependencies(String pluginName) {
List<String> ordered = new ArrayList<>();
Set<String> visited = new HashSet<>();
visited.add(pluginName);
collectDependencies(pluginName, visited, ordered);
return ordered;
}

private void collectDependencies(String pluginName, Set<String> visited, List<String> ordered) {
for (String dep : pluginDependencies.getOrDefault(pluginName, Collections.emptyList())) {
if (visited.add(dep)) {
collectDependencies(dep, visited, ordered);
ordered.add(dep);
}
}
}

private List<RegisteredPlugin> getDependents(String pluginName) {
List<RegisteredPlugin> dependents = new ArrayList<>();
for (RegisteredPlugin p : plugins.values()) {
Expand Down Expand Up @@ -515,6 +541,11 @@ public void unloadPlugins() {
public void enableAll() {
RegisteredMetaPlugin meta = getMetaPlugin();
if (meta != null) {
// Dependencies must be enabled before the meta plugin itself.
for (String depName : getTransitiveDependencies(meta.getName())) {
RegisteredPlugin dep = plugins.get(depName);
if (dep != null) enablePlugin(dep);
}
enablePlugin(meta);
}
for (RegisteredPlugin rp : plugins.values()) {
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/de_de.lang
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ xinbot.metaplugin.error.load_runtime=MetaPlugin kann während der Laufzeit nicht
xinbot.metaplugin.error.enable_runtime=MetaPlugin kann während der Laufzeit nicht aktiviert werden!
xinbot.metaplugin.error.disable_runtime=MetaPlugin kann während der Laufzeit nicht deaktiviert werden!
xinbot.metaplugin.error.unload_runtime=MetaPlugin kann während der Laufzeit nicht entladen werden!
xinbot.metaplugin.error.unload_dependency_runtime=Plugin %s kann während der Laufzeit nicht entladen werden: das MetaPlugin hängt davon ab!
xinbot.metaplugin.error.count=Der Bot muss genau ein MetaPlugin zum Starten haben! (Gefunden: %d)

# LangManager messages
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/en_us.lang
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ xinbot.metaplugin.error.load_runtime=Cannot load MetaPlugin during runtime!
xinbot.metaplugin.error.enable_runtime=Cannot enable MetaPlugin during runtime!
xinbot.metaplugin.error.disable_runtime=Cannot disable MetaPlugin during runtime!
xinbot.metaplugin.error.unload_runtime=Cannot unload MetaPlugin during runtime!
xinbot.metaplugin.error.unload_dependency_runtime=Cannot unload plugin %s during runtime: the MetaPlugin depends on it!
xinbot.metaplugin.error.count=Bot must have exactly one MetaPlugin to start! (Found: %d)

# LangManager messages
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/fr_fr.lang
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ xinbot.metaplugin.error.load_runtime=Impossible de charger le MetaPlugin pendant
xinbot.metaplugin.error.enable_runtime=Impossible d'activer le MetaPlugin pendant l'exécution !
xinbot.metaplugin.error.disable_runtime=Impossible de désactiver le MetaPlugin pendant l'exécution !
xinbot.metaplugin.error.unload_runtime=Impossible de décharger le MetaPlugin pendant l'exécution !
xinbot.metaplugin.error.unload_dependency_runtime=Impossible de décharger le plugin %s pendant l'exécution : le MetaPlugin en dépend !
xinbot.metaplugin.error.count=Le bot doit avoir exactement un MetaPlugin pour démarrer ! (Trouvé : %d)

# LangManager messages
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/ja_jp.lang
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ xinbot.metaplugin.error.load_runtime=実行中に MetaPlugin を読み込むこ
xinbot.metaplugin.error.enable_runtime=実行中に MetaPlugin を有効にすることはできません!
xinbot.metaplugin.error.disable_runtime=実行中に MetaPlugin を無効にすることはできません!
xinbot.metaplugin.error.unload_runtime=実行中に MetaPlugin をアンロードすることはできません!
xinbot.metaplugin.error.unload_dependency_runtime=実行中にプラグイン %s をアンロードすることはできません。MetaPlugin が依存しています!
xinbot.metaplugin.error.count=ボットを開始するには、正確に1つの MetaPlugin が必要です!(見つかった数: %d)

# LangManager messages
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/ru_ru.lang
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ xinbot.metaplugin.error.load_runtime=Невозможно загрузить Met
xinbot.metaplugin.error.enable_runtime=Невозможно включить MetaPlugin во время работы!
xinbot.metaplugin.error.disable_runtime=Невозможно отключить MetaPlugin во время работы!
xinbot.metaplugin.error.unload_runtime=Невозможно выгрузить MetaPlugin во время работы!
xinbot.metaplugin.error.unload_dependency_runtime=Невозможно выгрузить плагин %s во время работы: от него зависит MetaPlugin!
xinbot.metaplugin.error.count=Для запуска бота должен быть ровно один MetaPlugin! (Найдено: %d)

# LangManager messages
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/zh_cn.lang
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ xinbot.metaplugin.error.load_runtime=不允许在运行过程中加载元插件
xinbot.metaplugin.error.enable_runtime=不允许在运行过程中启用元插件!
xinbot.metaplugin.error.disable_runtime=不允许在运行过程中禁用元插件!
xinbot.metaplugin.error.unload_runtime=不允许在运行过程中卸载元插件!
xinbot.metaplugin.error.unload_dependency_runtime=不允许在运行过程中卸载插件 %s:该插件被元插件依赖!
xinbot.metaplugin.error.count=机器人启动必须有且仅有一个元插件!(当前找到:%d 个)

# LangManager messages
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/zh_tw.lang
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ xinbot.metaplugin.error.load_runtime=不允許在運行過程中載入元外掛
xinbot.metaplugin.error.enable_runtime=不允許在運行過程中啟用元外掛程式!
xinbot.metaplugin.error.disable_runtime=不允許在運行過程中停用元外掛程式!
xinbot.metaplugin.error.unload_runtime=不允許在運行過程中卸載元外掛程式!
xinbot.metaplugin.error.unload_dependency_runtime=不允許在運行過程中卸載外掛程式 %s:該外掛程式被元外掛程式依賴!
xinbot.metaplugin.error.count=機器人啟動必須有且僅有一個元外掛程式!(當前找到:%d 個)
xinbot.plugin.enabled=已啟用外掛程式:%s
xinbot.plugin.disabled=已停用外掛程式:%s
Expand Down
47 changes: 47 additions & 0 deletions src/test/java/xin/bbtt/mcbot/BotTestState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2024-2026 huangdihd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package xin.bbtt.mcbot;

import java.lang.reflect.Field;

/**
* Test-only access to {@link Bot}'s private state ({@code running}, {@code session}),
* so plugin tests can simulate startup/runtime conditions on the Bot singleton.
* Production code intentionally exposes no setters for these fields, so the
* reflection is confined to this single helper.
*/
public final class BotTestState {

private BotTestState() {
}

public static void setRunning(boolean running) throws ReflectiveOperationException {
set("running", running);
}

public static void clearSession() throws ReflectiveOperationException {
set("session", null);
}

@SuppressWarnings("PMD.AvoidAccessibilityAlteration")
private static void set(String fieldName, Object value) throws ReflectiveOperationException {
Field field = Bot.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(Bot.INSTANCE, value);
}
}
24 changes: 24 additions & 0 deletions src/test/java/xin/bbtt/mcbot/plugin/DummyLibPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package xin.bbtt.mcbot.plugin;

public class DummyLibPlugin implements Plugin {

@Override
public void onLoad() {
DummyMetaPlugin.events.add("lib-load");
}

@Override
public void onEnable() {
DummyMetaPlugin.events.add("lib-enable");
}

@Override
public void onDisable() {
DummyMetaPlugin.events.add("lib-disable");
}

@Override
public void onUnload() {
DummyMetaPlugin.events.add("lib-unload");
}
}
48 changes: 48 additions & 0 deletions src/test/java/xin/bbtt/mcbot/plugin/DummyMetaPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package xin.bbtt.mcbot.plugin;

import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
import xin.bbtt.mcbot.Server;

import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;

public class DummyMetaPlugin implements MetaPlugin {

// Shared lifecycle event log so tests can assert ordering across plugins.
public static final List<String> events = new ArrayList<>();

@Override
public void onLoad() {
events.add("meta-load");
}

@Override
public void onEnable() {
events.add("meta-enable");
}

@Override
public void onDisable() {
events.add("meta-disable");
}

@Override
public void onUnload() {
events.add("meta-unload");
}

@Override
public SocketAddress getServerSocketAddress() {
return null;
}

@Override
public Server getServer(ClientboundLoginPacket loginPacket) {
return null;
}

public static void reset() {
events.clear();
}
}
Loading
Loading