Port async catcher

This commit is contained in:
IzzelAliz 2020-10-08 13:42:51 +08:00
parent 9c359490aa
commit fcc143a24f
11 changed files with 402 additions and 2 deletions

View File

@ -7,7 +7,7 @@ A Bukkit server implementation utilizing Mixin.
| Minecraft | Forge | Status | Build |
| :----: | :----: | :---: | :---: |
| 1.16.x | 34.1.7 | ACTIVE | [![1.16 Status](https://img.shields.io/appveyor/build/IzzelAliz/arclight-16?style=flat-square)](https://ci.appveyor.com/project/IzzelAliz/arclight-16) |
| 1.15.x | 31.2.37 | ACTIVE | [![1.15 Status](https://img.shields.io/appveyor/build/IzzelAliz/arclight-15?style=flat-square)](https://ci.appveyor.com/project/IzzelAliz/arclight-15) |
| 1.15.x | 31.2.45 | ACTIVE | [![1.15 Status](https://img.shields.io/appveyor/build/IzzelAliz/arclight-15?style=flat-square)](https://ci.appveyor.com/project/IzzelAliz/arclight-15) |
| 1.14.x | 28.2.0 | [LEGACY](https://github.com/IzzelAliz/Arclight/releases/tag/1.0.6) | [![1.14 Status](https://img.shields.io/appveyor/build/IzzelAliz/arclight?style=flat-square)](https://ci.appveyor.com/project/IzzelAliz/arclight) |
* Legacy version still accepts pull requests.

View File

@ -2,6 +2,7 @@ package io.izzel.arclight.api;
import sun.reflect.CallerSensitive;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;
@ -11,6 +12,7 @@ public class Unsafe {
private static final sun.misc.Unsafe unsafe;
private static final MethodHandles.Lookup lookup;
private static final MethodHandle defineClass;
static {
try {
@ -22,11 +24,24 @@ public class Unsafe {
Object base = unsafe.staticFieldBase(field);
long offset = unsafe.staticFieldOffset(field);
lookup = (MethodHandles.Lookup) unsafe.getObject(base, offset);
defineClass = lookup.unreflect(ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ProtectionDomain.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> T getStatic(Class<?> cl, String name) {
try {
Unsafe.ensureClassInitialized(cl);
Field field = cl.getDeclaredField(name);
Object materialByNameBase = Unsafe.staticFieldBase(field);
long materialByNameOffset = Unsafe.staticFieldOffset(field);
return (T) Unsafe.getObject(materialByNameBase, materialByNameOffset);
} catch (Exception e) {
return null;
}
}
public static MethodHandles.Lookup lookup() {
return lookup;
}
@ -337,7 +352,12 @@ public class Unsafe {
}
public static Class<?> defineClass(String s, byte[] bytes, int i, int i1, ClassLoader classLoader, ProtectionDomain protectionDomain) {
return unsafe.defineClass(s, bytes, i, i1, classLoader, protectionDomain);
try {
return (Class<?>) defineClass.bindTo(classLoader).invoke(s, bytes, i , i1, protectionDomain);
} catch (Throwable throwable) {
throwException(throwable);
return null;
}
}
public static Class<?> defineAnonymousClass(Class<?> aClass, byte[] bytes, Object[] objects) {

View File

@ -3,10 +3,15 @@ package io.izzel.arclight.common.asm;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import io.izzel.arclight.common.mod.util.log.ArclightI18nLogger;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.spongepowered.asm.launch.MixinLaunchPlugin;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
@ -36,6 +41,7 @@ public class ArclightImplementer implements ILaunchPluginService {
this.transformerLoader = transformerLoader;
this.implementers.put("inventory", new InventoryImplementer());
this.implementers.put("switch", SwitchTableFixer.INSTANCE);
this.implementers.put("async", AsyncCatcher.INSTANCE);
}
@Override
@ -79,4 +85,15 @@ public class ArclightImplementer implements ILaunchPluginService {
public boolean processClass(Phase phase, ClassNode classNode, Type classType) {
throw new IllegalStateException("Outdated ModLauncher");
}
public static void loadArgs(InsnList list, MethodNode methodNode, Type[] types, int i) {
if (!Modifier.isStatic(methodNode.access)) {
list.add(new VarInsnNode(Opcodes.ALOAD, i));
i += 1;
}
for (Type type : types) {
list.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), i));
i += type.getSize();
}
}
}

View File

@ -0,0 +1,255 @@
package io.izzel.arclight.common.asm;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import io.izzel.arclight.api.Unsafe;
import io.izzel.arclight.common.mod.server.ArclightServer;
import io.izzel.arclight.i18n.ArclightConfig;
import io.izzel.arclight.i18n.conf.AsyncCatcherSpec;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.util.Constants;
import java.io.InputStreamReader;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class AsyncCatcher implements Implementer {
public static final AsyncCatcher INSTANCE = new AsyncCatcher();
private static final Marker MARKER = MarkerManager.getMarker("ASYNC_CATCHER");
private static final CallbackInfoReturnable<?> NOOP = new CallbackInfoReturnable<>("noop", false);
private static final AtomicInteger COUNTER = new AtomicInteger(0);
private final boolean dump;
private final boolean warn;
private final AsyncCatcherSpec.Operation defaultOp;
private final Map<String, Map<String, String>> reasons;
private final ClassLoader classLoader;
public AsyncCatcher() {
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
this.reasons = gson.fromJson(
new InputStreamReader(AsyncCatcher.class.getResourceAsStream("/async_catcher.json")),
new TypeToken<Map<String, Map<String, String>>>() {}.getType()
);
this.defaultOp = ArclightConfig.spec().getAsyncCatcher().getDefaultOp();
this.dump = ArclightConfig.spec().getAsyncCatcher().isDump();
this.warn = ArclightConfig.spec().getAsyncCatcher().isWarn();
this.classLoader = Thread.currentThread().getContextClassLoader();
}
@Override
public boolean processClass(ClassNode node, ILaunchPluginService.ITransformerLoader transformerLoader) {
Map<String, String> map = reasons.get(node.name);
if (map != null) {
boolean found = false;
List<MethodNode> methods = node.methods;
for (int i = 0, methodsSize = methods.size(); i < methodsSize; i++) {
MethodNode method = methods.get(i);
String reason = map.get(method.name + method.desc);
if (reason != null) {
found = true;
injectCheck(node, method, reason);
}
}
return found;
}
return false;
}
private void injectCheck(ClassNode node, MethodNode methodNode, String reason) {
ArclightImplementer.LOGGER.debug(MARKER, "Injecting {}/{}{} for reason {}", node.name, methodNode.name, methodNode.desc, reason);
AsyncCatcherSpec.Operation operation = ArclightConfig.spec().getAsyncCatcher().getOverrides().getOrDefault(reason, defaultOp);
InsnList insnList = new InsnList();
LabelNode labelNode = new LabelNode(new Label());
LabelNode labelNode1 = new LabelNode(new Label());
insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "io/izzel/arclight/common/mod/server/ArclightServer", "isPrimaryThread", "()Z"));
insnList.add(new JumpInsnNode(Opcodes.IFNE, labelNode));
instantiateCallback(node, methodNode, insnList);
insnList.add(new FieldInsnNode(Opcodes.GETSTATIC, Type.getType(AsyncCatcherSpec.Operation.class).getInternalName(), operation.name(), Type.getType(AsyncCatcherSpec.Operation.class).getDescriptor()));
insnList.add(new LdcInsnNode(reason));
insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getType(AsyncCatcher.class).getInternalName(), "checkOp", "(Ljava/util/function/Supplier;Lio/izzel/arclight/i18n/conf/AsyncCatcherSpec$Operation;Ljava/lang/String;)Lorg/spongepowered/asm/mixin/injection/callback/CallbackInfoReturnable;"));
Type returnType = Type.getMethodType(methodNode.desc).getReturnType();
boolean hasReturn = !returnType.equals(Type.VOID_TYPE);
if (hasReturn) {
insnList.add(new InsnNode(Opcodes.DUP));
}
insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getType(CallbackInfoReturnable.class).getInternalName(), "isCancelled", "()Z"));
insnList.add(new JumpInsnNode(Opcodes.IFEQ, hasReturn ? labelNode1 : labelNode));
if (hasReturn) {
insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getType(CallbackInfoReturnable.class).getInternalName(), getReturnAccessor(returnType), getReturnDescriptor(returnType)));
if (returnType.getSort() > Type.DOUBLE) {
insnList.add(new TypeInsnNode(Opcodes.CHECKCAST, returnType.getInternalName()));
}
insnList.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN)));
} else {
insnList.add(new InsnNode(Opcodes.RETURN));
}
insnList.add(labelNode1);
insnList.add(new InsnNode(Opcodes.POP));
insnList.add(labelNode);
methodNode.instructions.insert(insnList);
}
private void instantiateCallback(ClassNode node, MethodNode methodNode, InsnList insnList) {
MethodNode bridge;
if (Modifier.isPrivate(methodNode.access)) {
bridge = createBridge(node, methodNode);
} else {
bridge = methodNode;
}
ClassNode classNode = new ClassNode();
String desc = createImplType(node, methodNode, classNode, bridge);
insnList.add(new TypeInsnNode(Opcodes.NEW, classNode.name));
insnList.add(new InsnNode(Opcodes.DUP));
Type methodType = Type.getMethodType(methodNode.desc);
ArclightImplementer.loadArgs(insnList, methodNode, methodType.getArgumentTypes(), 0);
insnList.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, classNode.name, "<init>", desc));
}
private String createImplType(ClassNode node, MethodNode methodNode, ClassNode classNode, MethodNode bridge) {
classNode.version = Opcodes.V1_8;
classNode.name = node.name + "$AsyncCatcher$" + COUNTER.getAndIncrement();
classNode.access = Opcodes.ACC_SYNTHETIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL;
classNode.superName = "java/lang/Object";
classNode.interfaces.add(Type.getType(Supplier.class).getInternalName());
List<Type> types = new ArrayList<>();
if (!Modifier.isStatic(methodNode.access)) {
types.add(Type.getObjectType(node.name));
}
types.addAll(Arrays.asList(Type.getArgumentTypes(methodNode.desc)));
for (int i = 0; i < types.size(); i++) {
FieldNode fieldNode = new FieldNode(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, "x" + i, types.get(i).getDescriptor(), null, null);
classNode.fields.add(fieldNode);
}
MethodNode init = new MethodNode();
init.name = "<init>";
init.desc = Type.getMethodType(Type.VOID_TYPE, types.toArray(new Type[0])).getDescriptor();
init.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
init.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"));
int offset = 1;
for (int i = 0; i < types.size(); i++) {
init.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
init.instructions.add(new VarInsnNode(types.get(i).getOpcode(Opcodes.ILOAD), offset));
init.instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, "x" + i, types.get(i).getDescriptor()));
offset += types.get(i).getSize();
}
init.instructions.add(new InsnNode(Opcodes.RETURN));
classNode.methods.add(init);
MethodNode get = new MethodNode();
get.name = "get";
get.desc = "()Ljava/lang/Object;";
GeneratorAdapter adapter = new GeneratorAdapter(get, Opcodes.ACC_PUBLIC, get.name, get.desc);
for (int i = 0; i < types.size(); i++) {
adapter.loadThis();
adapter.getField(Type.getObjectType(classNode.name), "x" + i, types.get(i));
}
get.instructions.add(new MethodInsnNode(
Modifier.isStatic(methodNode.access) ? Opcodes.INVOKESTATIC : Opcodes.INVOKEVIRTUAL,
node.name, bridge.name, bridge.desc
));
adapter.valueOf(Type.getReturnType(bridge.desc));
adapter.returnValue();
classNode.methods.add(get);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
byte[] bytes = writer.toByteArray();
Unsafe.defineClass(Type.getObjectType(classNode.name).getClassName(), bytes, 0, bytes.length, this.classLoader, AsyncCatcher.class.getProtectionDomain());
ArclightImplementer.LOGGER.debug(MARKER, "Defined impl callback class {}", classNode.name);
return init.desc;
}
private MethodNode createBridge(ClassNode node, MethodNode methodNode) {
MethodNode ret = new MethodNode();
ret.name = methodNode.name + "$asyncCatcher$" + COUNTER.getAndIncrement();
ret.desc = methodNode.desc;
ret.access = Opcodes.ACC_PUBLIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC;
if (Modifier.isStatic(methodNode.access)) {
ret.access = ret.access | Opcodes.ACC_STATIC;
}
Type methodType = Type.getMethodType(methodNode.desc);
ArclightImplementer.loadArgs(ret.instructions, methodNode, methodType.getArgumentTypes(), 0);
int invokeCode = Modifier.isStatic(methodNode.access) ? Opcodes.INVOKESTATIC : Opcodes.INVOKESPECIAL;
ret.instructions.add(new MethodInsnNode(invokeCode, node.name, methodNode.name, methodNode.desc));
ret.instructions.add(new InsnNode(methodType.getReturnType().getOpcode(Opcodes.IRETURN)));
node.methods.add(ret);
ArclightImplementer.LOGGER.debug(MARKER, "Bridge method {}/{}{} created", node.name, ret.name, ret.desc);
return ret;
}
static String getReturnAccessor(org.objectweb.asm.Type returnType) {
if (returnType.getSort() == org.objectweb.asm.Type.OBJECT || returnType.getSort() == org.objectweb.asm.Type.ARRAY) {
return "getReturnValue";
}
return String.format("getReturnValue%s", returnType.getDescriptor());
}
static String getReturnDescriptor(org.objectweb.asm.Type returnType) {
if (returnType.getSort() == org.objectweb.asm.Type.OBJECT || returnType.getSort() == org.objectweb.asm.Type.ARRAY) {
return String.format("()%s", Constants.OBJECT_DESC);
}
return String.format("()%s", returnType.getDescriptor());
}
@SuppressWarnings("unchecked")
public static <T> CallbackInfoReturnable<T> checkOp(Supplier<T> method, AsyncCatcherSpec.Operation operation, String reason) throws Throwable {
if (INSTANCE.warn) {
ArclightImplementer.LOGGER.warn(MARKER, "Async " + reason);
}
IllegalStateException exception = new IllegalStateException("Asynchronous " + reason + "!");
if (INSTANCE.dump) {
ArclightImplementer.LOGGER.debug(MARKER, "Async " + reason, exception);
}
switch (operation) {
case NONE: return (CallbackInfoReturnable<T>) NOOP;
case EXCEPTION: throw exception;
case BLOCK: {
CallbackInfoReturnable<T> cir = new CallbackInfoReturnable<>(reason, true);
CompletableFuture<T> future = CompletableFuture.supplyAsync(method, ArclightServer.getMainThreadExecutor());
cir.setReturnValue(future.get(5, TimeUnit.SECONDS));
return cir;
}
case DISPATCH: {
ArclightServer.executeOnMainThread(method::get);
CallbackInfoReturnable<T> cir = new CallbackInfoReturnable<>(reason, true);
cir.cancel();
return cir;
}
}
throw new IllegalStateException("how this can happen?");
}
}

View File

@ -295,6 +295,9 @@ public abstract class MinecraftServerMixin extends RecursiveEventLoop<TickDelaye
private void executeModerately() {
this.drainTasks();
while (!processQueue.isEmpty()) {
processQueue.remove().run();
}
java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L);
}

View File

@ -15,9 +15,11 @@ import org.bukkit.craftbukkit.v.command.ColouredConsoleSender;
import java.io.File;
import java.util.Objects;
import java.util.concurrent.Executor;
public class ArclightServer {
private static final Executor mainThreadExecutor = ArclightServer::executeOnMainThread;
private static CraftServer server;
@SuppressWarnings("ConstantConditions")
@ -49,10 +51,26 @@ public class ArclightServer {
return Objects.requireNonNull(server);
}
public static boolean isPrimaryThread() {
if (server == null) {
return Thread.currentThread().equals(getMinecraftServer().getExecutionThread());
} else {
return server.isPrimaryThread();
}
}
public static MinecraftServer getMinecraftServer() {
return ServerLifecycleHooks.getCurrentServer();
}
public static void executeOnMainThread(Runnable runnable) {
((MinecraftServerBridge) getMinecraftServer()).bridge$queuedProcess(runnable);
}
public static Executor getMainThreadExecutor() {
return mainThreadExecutor;
}
public static World.Environment getEnvironment(RegistryKey<DimensionType> key) {
return BukkitRegistry.DIM_MAP.get(key);
}

View File

@ -0,0 +1,22 @@
{
"net/minecraft/world/server/ServerWorld": {
"func_72838_d(Lnet/minecraft/entity/Entity;)Z": "entity add",
"func_217465_m(Lnet/minecraft/entity/Entity;)V": "entity register",
"removeEntityComplete(Lnet/minecraft/entity/Entity;Z)V": "entity unregister"
},
"net/minecraft/world/server/ChunkManager$EntityTracker": {
"func_219399_a(Lnet/minecraft/entity/player/ServerPlayerEntity;)V": "player tracker clear",
"func_219400_b(Lnet/minecraft/entity/player/ServerPlayerEntity;)V": "player tracker update"
},
"net/minecraft/block/Block": {
"func_220082_b(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)V": "block place",
"func_196243_a(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)V": "block remove"
},
"net/minecraft/entity/LivingEntity": {
"func_195064_c(Lnet/minecraft/potion/EffectInstance;)Z": "effect add"
},
"net/minecraft/world/server/ChunkManager": {
"func_219210_a(Lnet/minecraft/entity/Entity;)V": "entity track",
"func_219231_b(Lnet/minecraft/entity/Entity;)V": "entity untrack"
}
}

View File

@ -0,0 +1,42 @@
package io.izzel.arclight.i18n.conf;
import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
import java.util.Map;
@ConfigSerializable
public class AsyncCatcherSpec {
@Setting("dump")
private boolean dump;
@Setting("warn")
private boolean warn;
@Setting("defaultOperation")
private Operation defaultOp;
@Setting("overrides")
private Map<String, Operation> overrides;
public boolean isDump() {
return dump;
}
public boolean isWarn() {
return warn;
}
public Operation getDefaultOp() {
return defaultOp;
}
public Map<String, Operation> getOverrides() {
return overrides;
}
public enum Operation {
NONE, DISPATCH, BLOCK, EXCEPTION
}
}

View File

@ -18,6 +18,9 @@ public class ConfigSpec {
@Setting("compatibility")
private CompatSpec compatSpec;
@Setting("async-catcher")
private AsyncCatcherSpec asyncCatcherSpec;
public int getVersion() {
return version;
}
@ -33,4 +36,8 @@ public class ConfigSpec {
public CompatSpec getCompat() {
return compatSpec;
}
public AsyncCatcherSpec getAsyncCatcher() {
return asyncCatcherSpec;
}
}

View File

@ -11,4 +11,11 @@ compatibility {
}
entity-property-overrides {
}
}
async-catcher {
dump = true
warn = true
defaultOperation = block
overrides {
}
}

View File

@ -81,4 +81,13 @@ comments {
]
locale.comment = "语言/国际化相关设置"
optimization.comment = "服务端优化相关设置"
async-catcher.comment = [
"异步捕获相关设置"
"Async Catcher 共有四种模式"
"NONE - 保持在当前线程执行"
"DISPATCH - 不阻塞地发布到主线程"
"BLOCK - 发不到主线程并等待结果"
"EXCEPTION - 抛出异常"
]
async-catcher.dump.comment = "是否在 debug 日志中打印堆栈信息"
}