diff --git a/paper-server/build.gradle.kts b/paper-server/build.gradle.kts index ea68653983..278743a5d3 100644 --- a/paper-server/build.gradle.kts +++ b/paper-server/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { mockitoAgent("org.mockito:mockito-core:5.14.1") { isTransitive = false } // Paper - configure mockito agent that is needed in newer java versions testImplementation("org.ow2.asm:asm-tree:9.7.1") testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest + implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling } paperweight { diff --git a/paper-server/patches/sources/net/minecraft/CrashReport.java.patch b/paper-server/patches/sources/net/minecraft/CrashReport.java.patch index 3c5566c233..4b3553ad32 100644 --- a/paper-server/patches/sources/net/minecraft/CrashReport.java.patch +++ b/paper-server/patches/sources/net/minecraft/CrashReport.java.patch @@ -1,7 +1,10 @@ --- a/net/minecraft/CrashReport.java +++ b/net/minecraft/CrashReport.java -@@ -36,6 +36,7 @@ +@@ -34,8 +34,10 @@ + private final SystemReport systemReport = new SystemReport(); + public CrashReport(String message, Throwable cause) { ++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(cause); // Paper this.title = message; this.exception = cause; + this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit diff --git a/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch b/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch new file mode 100644 index 0000000000..23f5297d2e --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch @@ -0,0 +1,10 @@ +--- a/net/minecraft/CrashReportCategory.java ++++ b/net/minecraft/CrashReportCategory.java +@@ -110,6 +110,7 @@ + } else { + this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount]; + System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length); ++ this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper + return this.stackTrace.length; + } + } diff --git a/paper-server/patches/sources/net/minecraft/network/Connection.java.patch b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch index 23369333ae..edd58978c1 100644 --- a/paper-server/patches/sources/net/minecraft/network/Connection.java.patch +++ b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch @@ -1,5 +1,22 @@ --- a/net/minecraft/network/Connection.java +++ b/net/minecraft/network/Connection.java +@@ -82,13 +82,13 @@ + marker.add(Connection.PACKET_MARKER); + }); + public static final Supplier NETWORK_WORKER_GROUP = Suppliers.memoize(() -> { +- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).build()); ++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + public static final Supplier NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> { +- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build()); ++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + public static final Supplier LOCAL_WORKER_GROUP = Suppliers.memoize(() -> { +- return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).build()); ++ return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; + private final PacketFlow receiving; @@ -96,6 +96,11 @@ private final Queue> pendingActions = Queues.newConcurrentLinkedQueue(); public Channel channel; @@ -46,7 +63,7 @@ } } -+ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) throwable.printStackTrace(); // Spigot ++ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper } protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet packet) { diff --git a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch index 583fa5f66e..6defed881e 100644 --- a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch @@ -143,7 +143,7 @@ thread.setDaemon(true); thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(DedicatedServer.LOGGER)); thread.start(); -@@ -126,13 +204,22 @@ +@@ -126,13 +204,23 @@ this.setPreventProxyConnections(dedicatedserverproperties.preventProxyConnections); this.setLocalIp(dedicatedserverproperties.serverIp); } @@ -152,6 +152,7 @@ + org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings")); + org.spigotmc.SpigotConfig.registerCommands(); + // Spigot end ++ io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // Paper - load mappings for stacktrace deobf and etc. + // Paper start - initialize global and world-defaults configuration + this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess()); + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); @@ -167,7 +168,7 @@ DedicatedServer.LOGGER.info("Default game type: {}", dedicatedserverproperties.gamemode); InetAddress inetaddress = null; -@@ -156,10 +243,23 @@ +@@ -156,10 +244,23 @@ return false; } @@ -192,7 +193,7 @@ DedicatedServer.LOGGER.warn("To change this, set \"online-mode\" to \"true\" in the server.properties file."); } -@@ -170,7 +270,7 @@ +@@ -170,7 +271,7 @@ if (!OldUsersConverter.serverReadyAfterUserconversion(this)) { return false; } else { @@ -201,7 +202,7 @@ this.debugSampleSubscriptionTracker = new DebugSampleSubscriptionTracker(this.getPlayerList()); this.tickTimeLogger = new RemoteSampleLogger(TpsDebugDimensions.values().length, this.debugSampleSubscriptionTracker, RemoteDebugSampleType.TICK_TIME); long i = Util.getNanos(); -@@ -178,13 +278,13 @@ +@@ -178,13 +279,13 @@ SkullBlockEntity.setup(this.services, this); GameProfileCache.setUsesAuthentication(this.usesAuthentication()); DedicatedServer.LOGGER.info("Preparing level \"{}\"", this.getLevelIdName()); @@ -217,7 +218,7 @@ } if (dedicatedserverproperties.enableQuery) { -@@ -197,7 +297,7 @@ +@@ -197,7 +298,7 @@ this.rconThread = RconThread.create(this); } @@ -226,7 +227,7 @@ Thread thread1 = new Thread(new ServerWatchdog(this)); thread1.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(DedicatedServer.LOGGER)); -@@ -293,6 +393,7 @@ +@@ -293,6 +394,7 @@ this.queryThreadGs4.stop(); } @@ -234,7 +235,7 @@ } @Override -@@ -302,8 +403,8 @@ +@@ -302,8 +404,8 @@ } @Override @@ -245,7 +246,7 @@ } public void handleConsoleInput(String command, CommandSourceStack commandSource) { -@@ -311,12 +412,22 @@ +@@ -311,12 +413,22 @@ } public void handleConsoleInputs() { @@ -269,7 +270,7 @@ } @Override -@@ -383,7 +494,7 @@ +@@ -383,7 +495,7 @@ @Override public boolean isUnderSpawnProtection(ServerLevel world, BlockPos pos, Player player) { @@ -278,7 +279,7 @@ return false; } else if (this.getPlayerList().getOps().isEmpty()) { return false; -@@ -541,16 +652,52 @@ +@@ -541,16 +653,52 @@ @Override public String getPluginNames() { @@ -335,7 +336,7 @@ } public void storeUsingWhiteList(boolean useWhitelist) { -@@ -660,4 +807,15 @@ +@@ -660,4 +808,15 @@ } } } diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch index c04e76d0bd..bbce62e0f2 100644 --- a/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch @@ -1,5 +1,18 @@ --- a/net/minecraft/server/network/ServerConnectionListener.java +++ b/net/minecraft/server/network/ServerConnectionListener.java +@@ -52,10 +52,10 @@ + + private static final Logger LOGGER = LogUtils.getLogger(); + public static final Supplier SERVER_EVENT_GROUP = Suppliers.memoize(() -> { +- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build()); ++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + public static final Supplier SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(() -> { +- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build()); ++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + final MinecraftServer server; + public volatile boolean running; @@ -100,16 +100,26 @@ Connection.configureSerialization(channelpipeline, PacketFlow.SERVERBOUND, false, (BandwidthDebugMonitor) null); diff --git a/paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch b/paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch index bf5a0c64b8..f291323de9 100644 --- a/paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch @@ -65,7 +65,7 @@ + try { + root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap()); + } catch (Exception exception) { -+ exception.printStackTrace(); ++ io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper + } + + if (root != null) { @@ -78,7 +78,7 @@ + try { + NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2)); + } catch (Exception exception) { -+ exception.printStackTrace(); ++ io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper + } + } + // CraftBukkit end diff --git a/paper-server/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java b/paper-server/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java new file mode 100644 index 0000000000..66b6011ee3 --- /dev/null +++ b/paper-server/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java @@ -0,0 +1,66 @@ +package io.papermc.paper.logging; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.checkerframework.checker.nullness.qual.NonNull; + +@Plugin( + name = "StacktraceDeobfuscatingRewritePolicy", + category = Core.CATEGORY_NAME, + elementType = "rewritePolicy", + printObject = true +) +public final class StacktraceDeobfuscatingRewritePolicy implements RewritePolicy { + private static final MethodHandle DEOBFUSCATE_THROWABLE; + + static { + try { + final Class cls = Class.forName("io.papermc.paper.util.StacktraceDeobfuscator"); + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + final VarHandle instanceHandle = lookup.findStaticVarHandle(cls, "INSTANCE", cls); + final Object deobfuscator = instanceHandle.get(); + DEOBFUSCATE_THROWABLE = lookup + .unreflect(cls.getDeclaredMethod("deobfuscateThrowable", Throwable.class)) + .bindTo(deobfuscator); + } catch (final ReflectiveOperationException ex) { + throw new IllegalStateException(ex); + } + } + + private StacktraceDeobfuscatingRewritePolicy() { + } + + @Override + public @NonNull LogEvent rewrite(final @NonNull LogEvent rewrite) { + final Throwable thrown = rewrite.getThrown(); + if (thrown != null) { + deobfuscateThrowable(thrown); + return new Log4jLogEvent.Builder(rewrite) + .setThrownProxy(null) + .build(); + } + return rewrite; + } + + private static void deobfuscateThrowable(final Throwable thrown) { + try { + DEOBFUSCATE_THROWABLE.invoke(thrown); + } catch (final Error e) { + throw e; + } catch (final Throwable e) { + throw new RuntimeException(e); + } + } + + @PluginFactory + public static @NonNull StacktraceDeobfuscatingRewritePolicy createPolicy() { + return new StacktraceDeobfuscatingRewritePolicy(); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/ObfHelper.java b/paper-server/src/main/java/io/papermc/paper/util/ObfHelper.java new file mode 100644 index 0000000000..9e6d48335b --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/ObfHelper.java @@ -0,0 +1,156 @@ +package io.papermc.paper.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import net.neoforged.srgutils.IMappingFile; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public enum ObfHelper { + INSTANCE; + + private final @Nullable Map mappingsByObfName; + private final @Nullable Map mappingsByMojangName; + + ObfHelper() { + final @Nullable Set maps = loadMappingsIfPresent(); + if (maps != null) { + this.mappingsByObfName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::obfName, map -> map)); + this.mappingsByMojangName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::mojangName, map -> map)); + } else { + this.mappingsByObfName = null; + this.mappingsByMojangName = null; + } + } + + public @Nullable Map mappingsByObfName() { + return this.mappingsByObfName; + } + + public @Nullable Map mappingsByMojangName() { + return this.mappingsByMojangName; + } + + /** + * Attempts to get the obf name for a given class by its Mojang name. Will + * return the input string if mappings are not present. + * + * @param fullyQualifiedMojangName fully qualified class name (dotted) + * @return mapped or original fully qualified (dotted) class name + */ + public String reobfClassName(final String fullyQualifiedMojangName) { + if (this.mappingsByMojangName == null) { + return fullyQualifiedMojangName; + } + + final ClassMapping map = this.mappingsByMojangName.get(fullyQualifiedMojangName); + if (map == null) { + return fullyQualifiedMojangName; + } + + return map.obfName(); + } + + /** + * Attempts to get the Mojang name for a given class by its obf name. Will + * return the input string if mappings are not present. + * + * @param fullyQualifiedObfName fully qualified class name (dotted) + * @return mapped or original fully qualified (dotted) class name + */ + public String deobfClassName(final String fullyQualifiedObfName) { + if (this.mappingsByObfName == null) { + return fullyQualifiedObfName; + } + + final ClassMapping map = this.mappingsByObfName.get(fullyQualifiedObfName); + if (map == null) { + return fullyQualifiedObfName; + } + + return map.mojangName(); + } + + private static @Nullable Set loadMappingsIfPresent() { + try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) { + if (mappingsInputStream == null) { + return null; + } + final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot + final Set classes = new HashSet<>(); + + final StringPool pool = new StringPool(); + for (final IMappingFile.IClass cls : mappings.getClasses()) { + final Map methods = new HashMap<>(); + final Map fields = new HashMap<>(); + final Map strippedMethods = new HashMap<>(); + + for (final IMappingFile.IMethod methodMapping : cls.getMethods()) { + methods.put( + pool.string(methodKey( + Objects.requireNonNull(methodMapping.getMapped()), + Objects.requireNonNull(methodMapping.getMappedDescriptor()) + )), + pool.string(Objects.requireNonNull(methodMapping.getOriginal())) + ); + + strippedMethods.put( + pool.string(pool.string(strippedMethodKey( + methodMapping.getMapped(), + methodMapping.getDescriptor() + ))), + pool.string(methodMapping.getOriginal()) + ); + } + for (final IMappingFile.IField field : cls.getFields()) { + fields.put( + pool.string(field.getMapped()), + pool.string(field.getOriginal()) + ); + } + + final ClassMapping map = new ClassMapping( + Objects.requireNonNull(cls.getMapped()).replace('/', '.'), + Objects.requireNonNull(cls.getOriginal()).replace('/', '.'), + Map.copyOf(methods), + Map.copyOf(fields), + Map.copyOf(strippedMethods) + ); + classes.add(map); + } + + return Set.copyOf(classes); + } catch (final IOException ex) { + System.err.println("Failed to load mappings."); + ex.printStackTrace(); + return null; + } + } + + public static String strippedMethodKey(final String methodName, final String methodDescriptor) { + final String methodKey = methodKey(methodName, methodDescriptor); + final int returnDescriptorEnd = methodKey.indexOf(')'); + return methodKey.substring(0, returnDescriptorEnd + 1); + } + + public static String methodKey(final String methodName, final String methodDescriptor) { + return methodName + methodDescriptor; + } + + public record ClassMapping( + String obfName, + String mojangName, + Map methodsByObf, + Map fieldsByObf, + // obf name with mapped desc to mapped name. return value is excluded from desc as reflection doesn't use it + Map strippedMethods + ) {} +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/paper-server/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java new file mode 100644 index 0000000000..242811578a --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java @@ -0,0 +1,144 @@ +package io.papermc.paper.util; + +import io.papermc.paper.configuration.GlobalConfiguration; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +@DefaultQualifier(NonNull.class) +public enum StacktraceDeobfuscator { + INSTANCE; + + private final Map, Int2ObjectMap> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) { + @Override + protected boolean removeEldestEntry(final Map.Entry, Int2ObjectMap> eldest) { + return this.size() > 127; + } + }); + + public void deobfuscateThrowable(final Throwable throwable) { + if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true + return; + } + + throwable.setStackTrace(this.deobfuscateStacktrace(throwable.getStackTrace())); + final Throwable cause = throwable.getCause(); + if (cause != null) { + this.deobfuscateThrowable(cause); + } + for (final Throwable suppressed : throwable.getSuppressed()) { + this.deobfuscateThrowable(suppressed); + } + } + + public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) { + if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true + return traceElements; + } + + final @Nullable Map mappings = ObfHelper.INSTANCE.mappingsByObfName(); + if (mappings == null || traceElements.length == 0) { + return traceElements; + } + final StackTraceElement[] result = new StackTraceElement[traceElements.length]; + for (int i = 0; i < traceElements.length; i++) { + final StackTraceElement element = traceElements[i]; + + final String className = element.getClassName(); + final String methodName = element.getMethodName(); + + final ObfHelper.ClassMapping classMapping = mappings.get(className); + if (classMapping == null) { + result[i] = element; + continue; + } + + final Class clazz; + try { + clazz = Class.forName(className); + } catch (final ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + final @Nullable String methodKey = this.determineMethodForLine(clazz, element.getLineNumber()); + final @Nullable String mappedMethodName = methodKey == null ? null : classMapping.methodsByObf().get(methodKey); + + result[i] = new StackTraceElement( + element.getClassLoaderName(), + element.getModuleName(), + element.getModuleVersion(), + classMapping.mojangName(), + mappedMethodName != null ? mappedMethodName : methodName, + sourceFileName(classMapping.mojangName()), + element.getLineNumber() + ); + } + return result; + } + + private @Nullable String determineMethodForLine(final Class clazz, final int lineNumber) { + return this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap).get(lineNumber); + } + + private static String sourceFileName(final String fullClassName) { + final int dot = fullClassName.lastIndexOf('.'); + final String className = dot == -1 + ? fullClassName + : fullClassName.substring(dot + 1); + final String rootClassName = className.split("\\$")[0]; + return rootClassName + ".java"; + } + + private static Int2ObjectMap buildLineMap(final Class key) { + final StringPool pool = new StringPool(); + final Int2ObjectMap lineMap = new Int2ObjectOpenHashMap<>(); + final class LineCollectingMethodVisitor extends MethodVisitor { + private final String name; + private final String descriptor; + + LineCollectingMethodVisitor(final String name, final String descriptor) { + super(Opcodes.ASM9); + this.name = name; + this.descriptor = descriptor; + } + + @Override + public void visitLineNumber(final int line, final Label start) { + lineMap.put(line, pool.string(ObfHelper.methodKey(this.name, this.descriptor))); + } + } + final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { + return new LineCollectingMethodVisitor(name, descriptor); + } + }; + try { + final @Nullable InputStream inputStream = StacktraceDeobfuscator.class.getClassLoader() + .getResourceAsStream(key.getName().replace('.', '/') + ".class"); + if (inputStream == null) { + throw new IllegalStateException("Could not find class file: " + key.getName()); + } + final byte[] classData; + try (inputStream) { + classData = inputStream.readAllBytes(); + } + final ClassReader reader = new ClassReader(classData); + reader.accept(classVisitor, 0); + } catch (final IOException ex) { + throw new RuntimeException(ex); + } + return lineMap; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/StringPool.java b/paper-server/src/main/java/io/papermc/paper/util/StringPool.java new file mode 100644 index 0000000000..c0a486cb46 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/StringPool.java @@ -0,0 +1,34 @@ +package io.papermc.paper.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +/** + * De-duplicates {@link String} instances without using {@link String#intern()}. + * + *

Interning may not be desired as we may want to use the heap for our pool, + * so it can be garbage collected as normal, etc.

+ * + *

Additionally, interning can be slow due to the potentially large size of the + * pool (as it is shared for the entire JVM), and because most JVMs implement + * it using JNI.

+ */ +@DefaultQualifier(NonNull.class) +public final class StringPool { + private final Map pool; + + public StringPool() { + this(new HashMap<>()); + } + + public StringPool(final Map map) { + this.pool = map; + } + + public String string(final String string) { + return this.pool.computeIfAbsent(string, Function.identity()); + } +} diff --git a/paper-server/src/main/java/org/spigotmc/WatchdogThread.java b/paper-server/src/main/java/org/spigotmc/WatchdogThread.java index c4bf7053d8..f697d45e0a 100644 --- a/paper-server/src/main/java/org/spigotmc/WatchdogThread.java +++ b/paper-server/src/main/java/org/spigotmc/WatchdogThread.java @@ -130,7 +130,7 @@ public class WatchdogThread extends Thread } log.log( Level.SEVERE, "\tStack:" ); // - for ( StackTraceElement stack : thread.getStackTrace() ) + for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace()) ) // Paper { log.log( Level.SEVERE, "\t\t" + stack ); } diff --git a/paper-server/src/main/resources/log4j2.xml b/paper-server/src/main/resources/log4j2.xml index 18e961a37b..128fa1376f 100644 --- a/paper-server/src/main/resources/log4j2.xml +++ b/paper-server/src/main/resources/log4j2.xml @@ -30,10 +30,14 @@ + + + + - +