Skip to content

Commit

Permalink
Use ClassBytecodeProcessors to give BungeeCord Plugin implementations…
Browse files Browse the repository at this point in the history
… state.

Don't reload a Plugin 10 times if 10 classes are reloaded.
  • Loading branch information
Connor Spencer Harries committed Apr 27, 2016
1 parent 5c463d5 commit e39275f
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 26 deletions.
Expand Up @@ -16,7 +16,10 @@

package ninja.smirking.rebel;

import ninja.smirking.rebel.transformer.LoadedPluginCBP;
import org.zeroturnaround.javarebel.ClassResourceSource;
import org.zeroturnaround.javarebel.Integration;
import org.zeroturnaround.javarebel.IntegrationFactory;
import org.zeroturnaround.javarebel.LoggerFactory;
import org.zeroturnaround.javarebel.Plugin;
import org.zeroturnaround.javarebel.ReloaderFactory;
Expand All @@ -32,8 +35,10 @@ public class BungeePlugin implements Plugin {

@Override
public void preinit() {
Integration integration = IntegrationFactory.getInstance();
integration.addIntegrationProcessor(new LoadedPluginCBP(), false);
ReloaderFactory.getInstance().addClassReloadListener(BungeeReloader.INSTANCE);
LoggerFactory.getInstance().echo("Plugins will be reloaded when class changes are detected.");
LoggerFactory.getInstance().infoEcho("Plugins will be reloaded when class changes are detected.");
}

@Override
Expand All @@ -42,7 +47,7 @@ public boolean checkDependencies(ClassLoader cl, ClassResourceSource crs) {
initialCheck = true;
isBungeeLoaded = crs.getClassResource("net.md_5.bungee.api.ProxyServer") != null;
if (!isBungeeLoaded) {
LoggerFactory.getInstance().echo("Could not find ProxyServer in your classpath!");
LoggerFactory.getInstance().warnEcho("Could not find ProxyServer in your classpath!");
}
}
return isBungeeLoaded;
Expand Down
Expand Up @@ -16,20 +16,30 @@

package ninja.smirking.rebel;

import com.google.common.collect.MapMaker;
import com.google.common.collect.Sets;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import ninja.smirking.rebel.bungee.DummyPlugin;
import org.zeroturnaround.javarebel.ClassEventListener;
import org.zeroturnaround.javarebel.Notifier;
import org.zeroturnaround.javarebel.NotifierFactory;

/**
* Attempts to disable and re-enable a {@link Plugin} when one of it's classes are reloaded.
*
* @author Connor Spencer Harries
*/
public enum BungeeReloader implements ClassEventListener {
enum BungeeReloader implements ClassEventListener {
INSTANCE;

private final Set<Plugin> reloading = Sets.newSetFromMap(new MapMaker().weakKeys().makeMap());

@Override
public void onClassEvent(int eventType, Class<?> klass) {
if (klass.getClassLoader() == ClassLoader.getSystemClassLoader()) {
Expand All @@ -38,8 +48,16 @@ public void onClassEvent(int eventType, Class<?> klass) {

for (Plugin plugin : ProxyServer.getInstance().getPluginManager().getPlugins()) {
if (plugin.getClass().getClassLoader() == klass.getClassLoader()) {
disable(plugin);
enable(plugin);
if (reloading.add(plugin)) {
ProxyServer.getInstance().getScheduler().schedule(DummyPlugin.getInstance(), () -> {
if (reloading.remove(plugin)) {
disable(plugin);
enable(plugin);

NotifierFactory.getInstance().notify("Minecraft Rebel", String.format("%s was reloaded", plugin.getDescription().getName()), Notifier.IDENotificationLevel.INFO, Notifier.IDENotificationType.RELOAD);
}
}, 3L, TimeUnit.SECONDS);
}
break;
}
}
Expand All @@ -51,29 +69,38 @@ public int priority() {
}

private void disable(Plugin plugin) {
try {
ProxyServer.getInstance().getLogger().log(Level.INFO, "Disabling plugin {0}", new Object[] {
plugin.getDescription().getName()
});
plugin.onDisable();
} catch (Throwable error) {
ProxyServer.getInstance().getLogger().log(Level.SEVERE, "Exception disabling plugin {0}: {1}", new Object[] {
plugin.getDescription().getName(), error
});
StatefulPlugin statefulPlugin = StatefulPlugin.class.cast(plugin);
if (statefulPlugin._rebel_isEnabled()) {
try {
ProxyServer.getInstance().getPluginManager().unregisterListeners(plugin);
ProxyServer.getInstance().getPluginManager().unregisterCommands(plugin);
ProxyServer.getInstance().getScheduler().cancel(plugin);

ProxyServer.getInstance().getLogger().log(Level.INFO, "Disabling plugin {0}", new Object[]{
plugin.getDescription().getName()
});
plugin.onDisable();
} catch (Throwable error) {
ProxyServer.getInstance().getLogger().log(Level.SEVERE, "Exception disabling plugin {0}: {1}", new Object[]{
plugin.getDescription().getName(), error
});
}
}
ProxyServer.getInstance().getScheduler().cancel(plugin);
}

private void enable(Plugin plugin) {
try {
plugin.onEnable();
ProxyServer.getInstance().getLogger().log(Level.INFO, "Enabled plugin {0} version {1} by {2}", new Object[] {
plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getAuthor()
});
} catch (Throwable error) {
ProxyServer.getInstance().getLogger().log(Level.SEVERE, "Exception encountered when enabling {0}: {1}", new Object[] {
plugin.getDescription().getName(), error
});
StatefulPlugin statefulPlugin = StatefulPlugin.class.cast(plugin);
if (!statefulPlugin._rebel_isEnabled()) {
try {
plugin.onEnable();
ProxyServer.getInstance().getLogger().log(Level.INFO, "Enabled plugin {0} version {1} by {2}", new Object[]{
plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getAuthor()
});
} catch (Throwable error) {
ProxyServer.getInstance().getLogger().log(Level.SEVERE, "Exception encountered when enabling {0}: {1}", new Object[]{
plugin.getDescription().getName(), error
});
}
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2016 Connor Spencer Harries
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ninja.smirking.rebel;

/**
* It's highly unlikely that a plugin implements methods with the names below, hence picking them over {@code
* isEnabled()}.
*
* @author Connor Spencer Harries
*/
@SuppressWarnings("WeakerAccess")
public interface StatefulPlugin {
void _rebel_setEnabled(boolean enabled);

boolean _rebel_isEnabled();
}
@@ -0,0 +1,27 @@
/*
* Copyright 2016 Connor Spencer Harries
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ninja.smirking.rebel.bungee;

import net.md_5.bungee.api.plugin.Plugin;

public class DummyPlugin extends Plugin {
private static final Plugin instance = new DummyPlugin();

public static Plugin getInstance() {
return instance;
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2016 Connor Spencer Harries
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ninja.smirking.rebel.transformer;

import net.md_5.bungee.api.plugin.PluginClassloader;
import org.zeroturnaround.bundled.javassist.ClassPool;
import org.zeroturnaround.bundled.javassist.CtClass;
import org.zeroturnaround.bundled.javassist.CtField;
import org.zeroturnaround.bundled.javassist.CtMethod;
import org.zeroturnaround.bundled.javassist.CtNewMethod;
import org.zeroturnaround.javarebel.LoggerFactory;
import org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor;

public class LoadedPluginCBP extends JavassistClassBytecodeProcessor {
private static final String PLUGIN_CLASS_NAME = "net.md_5.bungee.api.plugin.Plugin";

@Override
public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception {
if (cl == ClassLoader.getSystemClassLoader() || !(cl instanceof PluginClassloader)) {
return;
}

CtClass cursor = ctClass;
while (true) {
if (cursor.getName().equals(PLUGIN_CLASS_NAME)) {
break;
}
cursor = ctClass.getSuperclass();
if (cursor == null || cursor.getName().equals("java.lang.Object")) {
return;
}
}

LoggerFactory.getInstance().echo(String.format("Instrumenting class \"%s\"", ctClass.getName()));

// region State tracking
ctClass.addInterface(cp.get("ninja.smirking.rebel.StatefulPlugin"));

CtField field = CtField.make("private volatile boolean minecraft_rebel_enabled = false;", ctClass);
ctClass.addField(field);

CtMethod isEnabledMethod = CtNewMethod.make("public boolean _rebel_isEnabled() { return this.minecraft_rebel_enabled; }", ctClass);
ctClass.addMethod(isEnabledMethod);

CtMethod setEnabledMethod = CtNewMethod.make("public void _rebel_setEnabled(boolean enabled) { this.minecraft_rebel_enabled = enabled; }", ctClass);
ctClass.addMethod(setEnabledMethod);
// endregion State tracking

boolean implementsDisable = false;
boolean implementsEnable = false;
for (CtMethod method : ctClass.getDeclaredMethods()) {
if (method.getName().equals("onEnable")) {
implementsEnable = true;
method.setName("_rebel_onEnable");
method = CtNewMethod.make(
"public void onEnable() { " +
"_rebel_onEnable(); " +
"_rebel_setEnabled(true); " +
"}", ctClass);
ctClass.addMethod(method);
} else if (method.getName().equals("onDisable")) {
implementsDisable = true;
method.setName("_rebel_onDisable");
method = CtNewMethod.make(
"public void onDisable() { " +
"try { " +
"_rebel_onDisable(); " +
"} finally { " +
"_rebel_setEnabled(false); " +
"} " +
"}", ctClass);
ctClass.addMethod(method);
}
}

if (!implementsEnable) {
LoggerFactory.getInstance().warnEcho("{} does not implement onEnable", ctClass.getName());
CtMethod onEnable = CtNewMethod.make(
"public void onEnable() { " +
"_rebel_setEnabled(true); " +
"}", ctClass);
ctClass.addMethod(onEnable);
}

if (!implementsDisable) {
CtMethod onDisable = CtNewMethod.make(
"public void onDisable() { " +
"_rebel_setEnabled(false); " +
"}", ctClass);
ctClass.addMethod(onDisable);
}
}
}
2 changes: 0 additions & 2 deletions settings.gradle
@@ -1,5 +1,3 @@
rootProject.name = 'minecraft-rebel-plugin'
include 'bukkit-rebel-plugin'
include 'bungee-rebel-plugin'
include 'dummy'

0 comments on commit e39275f

Please sign in to comment.