Skip to content

Commit

Permalink
Add Sponge module
Browse files Browse the repository at this point in the history
  • Loading branch information
aromaa authored and skybber committed May 28, 2024
1 parent ded52f6 commit ea6a4cd
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 0 deletions.
6 changes: 6 additions & 0 deletions plugin/hotswap-agent-plugins/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -268,5 +268,11 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.hotswapagent</groupId>
<artifactId>hotswap-agent-sponge-plugin</artifactId>
<version>${project.version}</version>
</dependency>

</dependencies>
</project>
3 changes: 3 additions & 0 deletions plugin/hotswap-agent-sponge-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[Sponge](https://spongepowered.org/)
========================================
Reload event registrations after listener class change.
21 changes: 21 additions & 0 deletions plugin/hotswap-agent-sponge-plugin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.hotswapagent</groupId>
<artifactId>hotswap-agent-parent</artifactId>
<version>1.4.2-SNAPSHOT</version>
<relativePath>../../hotswap-agent-parent/pom.xml</relativePath>
</parent>

<artifactId>hotswap-agent-sponge-plugin</artifactId>

<dependencies>
<dependency>
<groupId>org.hotswapagent</groupId>
<artifactId>hotswap-agent-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2013-2024 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent 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 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent 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 HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.sponge;

import org.hotswap.agent.command.Command;
import org.hotswap.agent.logging.AgentLogger;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

final class ReloadListenersCommand implements Command {

private static final AgentLogger LOGGER = AgentLogger.getLogger(ReloadListenersCommand.class);

private final ClassLoader appClassLoader;
private final Set<Object> eventManagers;
private final Map<Object, MethodHandles.Lookup> lookups;
private final Set<Consumer<Class<?>>> callbacks;
private final Class<?> listenerClass;

ReloadListenersCommand(ClassLoader appClassLoader, Set<Object> eventManagers, Map<Object, MethodHandles.Lookup> lookups, Set<Consumer<Class<?>>> callbacks, Class<?> listenerClass) {
this.appClassLoader = appClassLoader;
this.eventManagers = eventManagers;
this.lookups = lookups;
this.callbacks = callbacks;
this.listenerClass = listenerClass;
}

@Override
public void executeCommand() {
try {
Class<?> spongeEventManagerClass = Class.forName("org.spongepowered.common.event.manager.SpongeEventManager", true, appClassLoader);
Class<?> registeredListenerClass = Class.forName("org.spongepowered.common.event.manager.RegisteredListener", true, appClassLoader);
Class<?> multiMapClass = Class.forName("com.google.common.collect.Multimap", true, appClassLoader);
Class<?> pluginContainerClass = Class.forName("org.spongepowered.plugin.PluginContainer", true, appClassLoader);

Field handlersByEventField = spongeEventManagerClass.getDeclaredField("handlersByEvent");
handlersByEventField.setAccessible(true);

Field lockField = spongeEventManagerClass.getDeclaredField("lock");
lockField.setAccessible(true);

Method unregisterListenersMethod = spongeEventManagerClass.getDeclaredMethod("unregisterListeners", Object.class);
Method registerListenersMethod = spongeEventManagerClass.getDeclaredMethod("registerListeners", pluginContainerClass, Object.class);
Method registerListenersLookupMethod = spongeEventManagerClass.getDeclaredMethod("registerListeners", pluginContainerClass, Object.class, MethodHandles.Lookup.class);

Method valuesMethod = multiMapClass.getMethod("values");

Method getHandleMethod = registeredListenerClass.getMethod("getHandle");
Method getPluginMethod = registeredListenerClass.getMethod("getPlugin");

HashMap<Object, HandleData> handles = new HashMap<>();
synchronized (eventManagers) {
for (Object eventManager : eventManagers) {
Object multiMap = handlersByEventField.get(eventManager);
synchronized (lockField.get(eventManager)) {
Collection<?> values = (Collection<?>) valuesMethod.invoke(multiMap);
for (Object listener : values) {
Object handle = getHandleMethod.invoke(listener);
if (!handle.getClass().equals(listenerClass)) {
continue;
}

Object plugin = getPluginMethod.invoke(listener);

handles.computeIfAbsent(handle, $ -> new HandleData(plugin))
.eventManagers.add(eventManager);
}
}
}
}

if (handles.isEmpty()) {
return;
}

for (Map.Entry<Object, HandleData> entry : handles.entrySet()) {
Object handle = entry.getKey();
HandleData data = entry.getValue();
MethodHandles.Lookup lookup = lookups.get(handle);
for (Object eventManager : data.eventManagers) {
unregisterListenersMethod.invoke(eventManager, handle);
if (lookup == null) {
registerListenersMethod.invoke(eventManager, data.plugin, handle);
} else {
registerListenersLookupMethod.invoke(eventManager, data.plugin, handle, lookup);
}
}
}

LOGGER.info("Successfully refreshed listeners for {}", listenerClass);

for (Consumer<Class<?>> callback : callbacks) {
try {
callback.accept(listenerClass);
} catch (Throwable e) {
LOGGER.error("Listener update callback threw exception {}", e, listenerClass);
}
}
} catch (Throwable e) {
LOGGER.error("Error refreshing listeners for {}", e, listenerClass);
}
}

private static final class HandleData {

private final Object plugin;
private final Set<Object> eventManagers;

private HandleData(Object plugin) {
this.plugin = plugin;
this.eventManagers = new HashSet<>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2013-2024 the HotswapAgent authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent 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 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent 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 HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.plugin.sponge;

import org.hotswap.agent.annotation.Init;
import org.hotswap.agent.annotation.LoadEvent;
import org.hotswap.agent.annotation.OnClassLoadEvent;
import org.hotswap.agent.annotation.Plugin;
import org.hotswap.agent.command.Scheduler;
import org.hotswap.agent.javassist.CannotCompileException;
import org.hotswap.agent.javassist.ClassPool;
import org.hotswap.agent.javassist.CtClass;
import org.hotswap.agent.javassist.NotFoundException;
import org.hotswap.agent.util.PluginManagerInvoker;

import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

@Plugin(name = "Sponge",
description = "Registers newly added listeners to existing listener class",
testedVersions = { "11.0.0" },
expectedVersions = { "11.0.0" })
public final class SpongePlugin {

@Init
Scheduler scheduler;

@Init
ClassLoader appClassLoader;

private final Set<Object> eventManagers = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
private final Map<Object, MethodHandles.Lookup> lookups = Collections.synchronizedMap(new IdentityHashMap<>());
private final Set<Consumer<Class<?>>> callbacks = Collections.synchronizedSet(new HashSet<>());

@OnClassLoadEvent(classNameRegexp = "org.spongepowered.common.event.manager.SpongeEventManager")
public static void registerManager(CtClass ctClass, ClassPool classPool) throws NotFoundException, CannotCompileException {
StringBuilder initialization = new StringBuilder("{");
initialization.append(PluginManagerInvoker.buildInitializePlugin(SpongePlugin.class));
initialization.append(PluginManagerInvoker.buildCallPluginMethod(SpongePlugin.class, "registerEventManager", "this", "java.lang.Object"));
initialization.append("}");

ctClass.getDeclaredConstructor(new CtClass[0]).insertAfter(initialization.toString());

StringBuilder captureHandleLookup = new StringBuilder("{");
captureHandleLookup.append(PluginManagerInvoker.buildCallPluginMethod(SpongePlugin.class, "captureHandleLookup", "listenerObject", "java.lang.Object", "customLookup", "java.lang.invoke.MethodHandles$Lookup"));
captureHandleLookup.append("}");

ctClass.getDeclaredMethod("registerListener", new CtClass[] {
classPool.get("org.spongepowered.plugin.PluginContainer"),
classPool.get("java.lang.Object"),
classPool.get("java.lang.invoke.MethodHandles$Lookup"),
}).insertBefore(captureHandleLookup.toString());
}

public void registerEventManager(Object eventManager) {
eventManagers.add(eventManager);
}

public void captureHandleLookup(Object handle, MethodHandles.Lookup lookup) {
if (lookup == null) {
return;
}

lookups.put(handle, lookup);
}

@OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE)
public void reloadListeners(Class<?> clazz) {
scheduler.scheduleCommand(new ReloadListenersCommand(appClassLoader, eventManagers, lookups, callbacks, clazz));
}

public void addCallback(Consumer<Class<?>> callback) {
callbacks.add(callback);
}
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<module>plugin/hotswap-agent-velocity-plugin</module>
<module>plugin/hotswap-agent-plugins</module>
<module>plugin/hotswap-agent-hibernate-jakarta-plugin</module>
<module>plugin/hotswap-agent-sponge-plugin</module>
<module>hotswap-agent</module>
</modules>

Expand Down

0 comments on commit ea6a4cd

Please sign in to comment.