Fix Java 15 (#1025)
- Implemented a fix for the incompatibility with Java 15. This incompatibility was caused by the fact that the lambda generated in the NMS.NetworkManager is a hidden class in J15. Starting in Java 15, final fields in hidden classes can no longer be modified regardless of the 'accessible' flag. (see https://openjdk.java.net/jeps/371 "Using a hidden class", point 3). To circumvent this issue, this retrieves the data from the existing fields in the hidden class (a runnable or a callable) other than the packet. It then retrieves the constructor of the hidden class and instantiates it using the previously-retrieved data and the modified packet instance (this code is only used if the packet instance changed). - Introduced a new ObjectReconstructor class that does all the fields/constructor discovering/accessing etc. The Runnable and Callable methods each get one instance of this class so that we can avoid having to get the fields/constructors and set them accessible every time we want to replace a packet. Co-authored-by: Mark Vainomaa <mikroskeem@mikroskeem.eu>
This commit is contained in:
parent
7bac4ec634
commit
bbb053aa4e
|
@ -34,6 +34,8 @@ import com.comphenix.protocol.utility.MinecraftFields;
|
||||||
import com.comphenix.protocol.utility.MinecraftMethods;
|
import com.comphenix.protocol.utility.MinecraftMethods;
|
||||||
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
|
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
|
||||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||||
|
import com.comphenix.protocol.utility.ObjectReconstructor;
|
||||||
|
import com.comphenix.protocol.wrappers.Pair;
|
||||||
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
import com.comphenix.protocol.wrappers.WrappedGameProfile;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
@ -48,6 +50,7 @@ import org.apache.commons.lang.Validate;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
@ -55,8 +58,8 @@ import java.net.SocketAddress;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a channel injector.
|
* Represents a channel injector.
|
||||||
* @author Kristian
|
* @author Kristian
|
||||||
|
@ -88,8 +91,23 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new RuntimeException("Encountered an error caused by a reload! Please properly restart your server!", ex);
|
throw new RuntimeException("Encountered an error caused by a reload! Please properly restart your server!", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Method hiddenClassMethod = null;
|
||||||
|
try {
|
||||||
|
if (Float.parseFloat(System.getProperty("java.class.version")) >= 59) {
|
||||||
|
hiddenClassMethod = Class.class.getMethod("isHidden");
|
||||||
|
}
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
IS_HIDDEN_CLASS = hiddenClassMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Starting in Java 15 (59), the lambdas are hidden classes and we cannot use reflection to update
|
||||||
|
// the values anymore. Instead, the object will have to be reconstructed.
|
||||||
|
private static final Map<Class<?>, ObjectReconstructor<?>> RECONSTRUCTORS = new ConcurrentHashMap<>();
|
||||||
|
private static final Method IS_HIDDEN_CLASS;
|
||||||
|
|
||||||
// Saved accessors
|
// Saved accessors
|
||||||
private Method decodeBuffer;
|
private Method decodeBuffer;
|
||||||
private Method encodeBuffer;
|
private Method encodeBuffer;
|
||||||
|
@ -295,18 +313,18 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected <T> Callable<T> onMessageScheduled(final Callable<T> callable, FieldAccessor packetAccessor) {
|
protected <T> Callable<T> onMessageScheduled(final Callable<T> callable, FieldAccessor packetAccessor) {
|
||||||
final PacketEvent event = handleScheduled(callable, packetAccessor);
|
Pair<Callable<T>, PacketEvent> handled = handleScheduled(callable, packetAccessor);
|
||||||
|
|
||||||
// Handle cancelled events
|
// Handle cancelled events
|
||||||
if (event != null && event.isCancelled())
|
if (handled.getSecond() != null && handled.getSecond().isCancelled())
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return () -> {
|
return () -> {
|
||||||
T result;
|
T result;
|
||||||
|
|
||||||
// This field must only be updated in the pipeline thread
|
// This field must only be updated in the pipeline thread
|
||||||
currentEvent = event;
|
currentEvent = handled.getSecond();
|
||||||
result = callable.call();
|
result = handled.getFirst().call();
|
||||||
currentEvent = null;
|
currentEvent = null;
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -314,20 +332,20 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Runnable onMessageScheduled(final Runnable runnable, FieldAccessor packetAccessor) {
|
protected Runnable onMessageScheduled(final Runnable runnable, FieldAccessor packetAccessor) {
|
||||||
final PacketEvent event = handleScheduled(runnable, packetAccessor);
|
Pair<Runnable, PacketEvent> handled = handleScheduled(runnable, packetAccessor);
|
||||||
|
|
||||||
// Handle cancelled events
|
// Handle cancelled events
|
||||||
if (event != null && event.isCancelled())
|
if (handled.getSecond() != null && handled.getSecond().isCancelled())
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return () -> {
|
return () -> {
|
||||||
currentEvent = event;
|
currentEvent = handled.getSecond();
|
||||||
runnable.run();
|
handled.getFirst().run();
|
||||||
currentEvent = null;
|
currentEvent = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
|
<T> Pair<T, PacketEvent> handleScheduled(T instance, FieldAccessor accessor) {
|
||||||
// Let the filters handle this packet
|
// Let the filters handle this packet
|
||||||
Object original = accessor.get(instance);
|
Object original = accessor.get(instance);
|
||||||
|
|
||||||
|
@ -338,9 +356,9 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
PacketEvent result = new PacketEvent(ChannelInjector.class);
|
PacketEvent result = new PacketEvent(ChannelInjector.class);
|
||||||
result.setNetworkMarker(marker);
|
result.setNetworkMarker(marker);
|
||||||
return result;
|
return new Pair<>(instance, result);
|
||||||
} else {
|
} else {
|
||||||
return BYPASSED_PACKET;
|
return new Pair<>(instance, BYPASSED_PACKET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,11 +368,12 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
|
|
||||||
// Change packet to be scheduled
|
// Change packet to be scheduled
|
||||||
if (original != changed) {
|
if (original != changed) {
|
||||||
accessor.set(instance, changed);
|
instance = (T) (isHiddenClass(instance.getClass()) ?
|
||||||
|
updatePacketMessageReconstruct(instance, changed, accessor) :
|
||||||
|
updatePacketMessageSetReflection(instance, changed, accessor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return new Pair<>(instance, event != null ? event : BYPASSED_PACKET);
|
||||||
return event != null ? event : BYPASSED_PACKET;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -363,6 +382,30 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the packet in a packet message using a {@link FieldAccessor}.
|
||||||
|
*/
|
||||||
|
private static Object updatePacketMessageSetReflection(Object instance, Object newPacket, FieldAccessor accessor) {
|
||||||
|
accessor.set(instance, newPacket);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the packet in a packet message using a {@link ObjectReconstructor}.
|
||||||
|
*/
|
||||||
|
private static Object updatePacketMessageReconstruct(Object instance, Object newPacket, FieldAccessor accessor) {
|
||||||
|
final ObjectReconstructor<?> objectReconstructor =
|
||||||
|
RECONSTRUCTORS.computeIfAbsent(instance.getClass(), ObjectReconstructor::new);
|
||||||
|
|
||||||
|
final Object[] values = objectReconstructor.getValues(instance);
|
||||||
|
final Field[] fields = objectReconstructor.getFields();
|
||||||
|
for (int idx = 0; idx < fields.length; ++idx)
|
||||||
|
if (fields[idx].equals(accessor.getField()))
|
||||||
|
values[idx] = newPacket;
|
||||||
|
|
||||||
|
return objectReconstructor.reconstruct(values);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the given object is a compressor or decompressor.
|
* Determine if the given object is a compressor or decompressor.
|
||||||
* @param handler - object to test.
|
* @param handler - object to test.
|
||||||
|
@ -943,4 +986,15 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
|
||||||
public Channel getChannel() {
|
public Channel getChannel() {
|
||||||
return originalChannel;
|
return originalChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isHiddenClass(Class<?> clz) {
|
||||||
|
if (IS_HIDDEN_CLASS == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (Boolean) IS_HIDDEN_CLASS.invoke(clz);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to determine whether class '" + clz.getName() + "' is hidden or not", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.comphenix.protocol.utility;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class can be used to reconstruct objects.
|
||||||
|
*
|
||||||
|
* Note that it is limited to classes where both the order and number of member variables matches the order and number
|
||||||
|
* of arguments for the first constructor. This means that this class is mostly useful for classes generated by lambdas.
|
||||||
|
*
|
||||||
|
* @param <T> The type of the object to reconstruct.
|
||||||
|
* @author Pim
|
||||||
|
*/
|
||||||
|
public class ObjectReconstructor<T> {
|
||||||
|
|
||||||
|
private final Class<T> clz;
|
||||||
|
private final Field[] fields;
|
||||||
|
private final Constructor<?> ctor;
|
||||||
|
|
||||||
|
public ObjectReconstructor(final Class<T> clz) {
|
||||||
|
this.clz = clz;
|
||||||
|
this.fields = clz.getDeclaredFields();
|
||||||
|
for (Field field : fields)
|
||||||
|
field.setAccessible(true);
|
||||||
|
this.ctor = clz.getDeclaredConstructors()[0];
|
||||||
|
this.ctor.setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the values of all member variables of the provided instance.
|
||||||
|
* @param instance The instance for which to get all the member variables.
|
||||||
|
* @return The values of the member variables from the instance.
|
||||||
|
*/
|
||||||
|
public Object[] getValues(final Object instance) {
|
||||||
|
final Object[] values = new Object[fields.length];
|
||||||
|
for (int idx = 0; idx < fields.length; ++idx)
|
||||||
|
try {
|
||||||
|
values[idx] = fields[idx].get(instance);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException("Failed to access field: " + fields[idx].getName() +
|
||||||
|
" for class: " + clz.getName(), e);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the fields in the class.
|
||||||
|
* @return The fields.
|
||||||
|
*/
|
||||||
|
public Field[] getFields() {
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the class using the new values.
|
||||||
|
* @param values The new values for the member variables of the class.
|
||||||
|
* @return The new instance.
|
||||||
|
*/
|
||||||
|
public T reconstruct(final Object[] values) {
|
||||||
|
if (values.length != fields.length)
|
||||||
|
throw new RuntimeException("Mismatched number of arguments for class: " + clz.getName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (T) ctor.newInstance(values);
|
||||||
|
} catch (InstantiationException e) {
|
||||||
|
throw new RuntimeException("Failed to reconstruct object of type: " + clz.getName(), e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException("Failed to access constructor of type: " + clz.getName(), e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new RuntimeException("Failed to invoke constructor of type: " + clz.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue