diff --git a/src/main/java/com/falsepattern/lib/internal/asm/CoreLoadingPlugin.java b/src/main/java/com/falsepattern/lib/internal/asm/CoreLoadingPlugin.java index a1fb61d..a9c951e 100644 --- a/src/main/java/com/falsepattern/lib/internal/asm/CoreLoadingPlugin.java +++ b/src/main/java/com/falsepattern/lib/internal/asm/CoreLoadingPlugin.java @@ -25,6 +25,8 @@ package com.falsepattern.lib.internal.asm; import com.falsepattern.lib.internal.FPLog; import com.falsepattern.lib.internal.Tags; import com.falsepattern.lib.internal.impl.dependencies.DependencyLoaderImpl; +import com.falsepattern.lib.internal.logging.CrashImprover; +import com.falsepattern.lib.internal.logging.NotEnoughVerbosity; import com.falsepattern.lib.mapping.MappingManager; import com.falsepattern.lib.turboasm.MergeableTurboTransformer; import lombok.Getter; @@ -75,6 +77,8 @@ public class CoreLoadingPlugin implements IFMLLoadingPlugin { FPLog.LOG.info("Scanned in " + (end - start) / 1000000 + "ms"); //Initializing the rest MappingManager.initialize(); + NotEnoughVerbosity.apply(); + CrashImprover.probe(); } @SuppressWarnings("SameParameterValue") diff --git a/src/main/java/com/falsepattern/lib/internal/asm/FPTransformer.java b/src/main/java/com/falsepattern/lib/internal/asm/FPTransformer.java index 5083918..32b948a 100644 --- a/src/main/java/com/falsepattern/lib/internal/asm/FPTransformer.java +++ b/src/main/java/com/falsepattern/lib/internal/asm/FPTransformer.java @@ -25,6 +25,7 @@ package com.falsepattern.lib.internal.asm; import com.falsepattern.lib.StableAPI; import com.falsepattern.lib.internal.asm.transformers.ConfigOrderTransformer; +import com.falsepattern.lib.internal.asm.transformers.CrashReportImprover; import com.falsepattern.lib.internal.asm.transformers.MixinPluginTransformer; import com.falsepattern.lib.internal.asm.transformers.TypeDiscovererModuleInfoSilencer; import com.falsepattern.lib.internal.impl.optifine.OptiFineTransformerHooksImpl; @@ -42,6 +43,7 @@ public class FPTransformer extends MergeableTurboTransformer { public FPTransformer() { super(Arrays.asList(new MixinPluginTransformer(), + new CrashReportImprover(), new TypeDiscovererModuleInfoSilencer(), new ConfigOrderTransformer())); } diff --git a/src/main/java/com/falsepattern/lib/internal/asm/transformers/CrashReportImprover.java b/src/main/java/com/falsepattern/lib/internal/asm/transformers/CrashReportImprover.java new file mode 100644 index 0000000..a0ea91f --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/asm/transformers/CrashReportImprover.java @@ -0,0 +1,85 @@ +/* + * This file is part of FalsePatternLib. + * + * Copyright (C) 2022-2024 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * FalsePatternLib is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FalsePatternLib 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FalsePatternLib. If not, see . + */ + +package com.falsepattern.lib.internal.asm.transformers; + +import com.falsepattern.lib.internal.Tags; +import com.falsepattern.lib.mapping.MappingManager; +import com.falsepattern.lib.mapping.types.MappingType; +import com.falsepattern.lib.mapping.types.NameType; +import com.falsepattern.lib.mapping.types.UniversalClass; +import com.falsepattern.lib.turboasm.ClassNodeHandle; +import com.falsepattern.lib.turboasm.TurboClassTransformer; +import lombok.val; +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodInsnNode; + +public class CrashReportImprover implements TurboClassTransformer { + + @Override + public String owner() { + return Tags.MODNAME; + } + + @Override + public String name() { + return "CrashReportImprover"; + } + + @Override + public boolean shouldTransformClass(@NotNull String className, @NotNull ClassNodeHandle classNode) { + return "net.minecraft.crash.CrashReport".equals(className); + } + + @Override + public boolean transformClass(@NotNull String className, @NotNull ClassNodeHandle classNode) { + val cn = classNode.getNode(); + if (cn == null) + return false; + for (val method: cn.methods) { + switch (method.name) { + case "saveToFile", "func_147149_a" -> {} + default -> { + continue; + } + } + val insnList = method.instructions.iterator(); + while (insnList.hasNext()) { + val insn = insnList.next(); + if ( insn.getOpcode() == Opcodes.INVOKEVIRTUAL && + insn instanceof MethodInsnNode call && + "java/io/FileWriter".equals(call.owner) && + "close".equals(call.name) && + "()V".equals(call.desc)) { + insnList.previous(); + insnList.add(new InsnNode(Opcodes.DUP)); + insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/falsepattern/lib/internal/logging/CrashImprover", "injectLatest", "(Ljava/io/FileWriter;)V", false)); + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/com/falsepattern/lib/internal/logging/CrashImprover.java b/src/main/java/com/falsepattern/lib/internal/logging/CrashImprover.java new file mode 100644 index 0000000..3938ac5 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/logging/CrashImprover.java @@ -0,0 +1,71 @@ +/* + * This file is part of FalsePatternLib. + * + * Copyright (C) 2022-2024 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * FalsePatternLib is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FalsePatternLib 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FalsePatternLib. If not, see . + */ + +package com.falsepattern.lib.internal.logging; + +import com.falsepattern.lib.internal.FPLog; +import lombok.val; + +import net.minecraft.launchwrapper.Launch; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class CrashImprover { + private static final String cookie = UUID.randomUUID().toString(); + static { + FPLog.LOG.info("Magic cookie: {}", cookie); + } + public static void probe() { + + } + + public static void injectLatest(FileWriter writer) { + val potentialLogs = Arrays.asList(new File(Launch.minecraftHome, "logs/fml-client-latest.log"), + new File(Launch.minecraftHome, "logs/fml-server-latest.log")); + for (val file: potentialLogs) { + try(InputStream is = new FileInputStream(file)) { + List lines = readLines(is); + if(lines.stream().anyMatch(l -> l.contains(cookie))) { + writer.append("\n\n").append("Complete log:").append('\n'); + for (String line : lines) { + writer.append(line).append('\n'); + } + } + } catch (IOException e) { + } + } + } + private static List readLines(InputStream stream) { + return new BufferedReader(new InputStreamReader(stream)).lines().collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/falsepattern/lib/internal/logging/NotEnoughVerbosity.java b/src/main/java/com/falsepattern/lib/internal/logging/NotEnoughVerbosity.java new file mode 100644 index 0000000..deb164b --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/logging/NotEnoughVerbosity.java @@ -0,0 +1,193 @@ +/* + * This file is part of FalsePatternLib. + * + * Copyright (C) 2022-2024 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * FalsePatternLib is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FalsePatternLib 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FalsePatternLib. If not, see . + */ + +package com.falsepattern.lib.internal.logging; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import com.falsepattern.lib.internal.FPLog; +import com.falsepattern.lib.internal.Tags; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.impl.Log4jContextFactory; +import org.apache.logging.log4j.spi.LoggerContextFactory; + +import net.minecraft.launchwrapper.Launch; + +// This class is a port of NotEnoughVerbosity, licensed under the Unlicense +// It has been merged into FalsePatternLib so that it's available inside more modpacks +// CHANGES: +// This variant will inspect the blackboard for an entry called "notenoughverbosity". If it's present, it will no-op. +// This will allow other people's lib mods to pull in this class without each one of them trying to reapply. +public class NotEnoughVerbosity { + + public static final Logger LOGGER = FPLog.LOG; + + public static void apply() { + try { + if (needsToRun()) { + reconfigureLog4j(); + } + } catch (Throwable t) { + LOGGER.error("NotEnoughVerbosity failed to apply!", t); + } + } + + private static boolean needsToRun() { + try { + Class.forName("io.github.legacymoddingmc.notenoughverbosity.NotEnoughVerbosity"); + // OG mod already present, do nothing + return false; + } catch (Throwable ignored) {} + + if (Launch.blackboard.containsKey("notenoughverbosity")) { + // Other mod already present + return false; + } + + boolean foundBadRoot = false; + for (LoggerContext context : getLoggerContexts()) { + LoggerConfig rootConfig = context.getConfiguration().getLoggers().get(""); + if(rootConfig != null) { + if(rootConfig.getLevel().intLevel() < Level.ALL.intLevel()) { + LOGGER.warn("Found root logger with verbosity " + rootConfig.getLevel() + ", will try to switch to Forge config."); + foundBadRoot = true; + break; + } else { + LOGGER.debug("Found root logger with verbosity " + rootConfig.getLevel() + ", this is fine."); + } + } else { + throw new RuntimeException("Couldn't find root logger."); + } + } + if(!foundBadRoot) { + LOGGER.debug("Root config seems fine, log4j will not be reconfigured."); + } + return foundBadRoot; + } + + public static void reconfigureLog4j() { + URI log4jUri = findForgeLog4j(); + if (log4jUri != null) { + LOGGER.info("Reconfiguring logger to use config at " + log4jUri); + LOGGER.info("New messages will go to fml-x-latest.log"); + String cookie = UUID.randomUUID().toString(); + LOGGER.info("Magic cookie: " + cookie); + setLog4jConfig(log4jUri); + int count = getLoggerContexts().size(); + LOGGER.info("Reconfigured logger (" + count + " context" + (count != 1 ? "s" : "") + ") to use config at " + log4jUri); + LOGGER.info("Earlier messages may be located in latest.log"); + + List latestLogLines = readLatestLog(cookie); + if(latestLogLines != null) { + LOGGER.info("Found earlier messages:\n-----Begin latest.log-----\n" + String.join("\n", latestLogLines) + "\n-----End latest.log-----"); + } + Launch.blackboard.put("notenoughverbosity", Tags.MODID); + } else { + LOGGER.warn("Could not find Forge's log4j2.xml on classpath, doing nothing"); + } + } + + private static List readLatestLog(String cookie) { + File latestLog = new File(Launch.minecraftHome, "logs/latest.log"); + if(!latestLog.exists()) { + LOGGER.debug("Couldn't find latest.log at " + latestLog.getAbsolutePath()); + return null; + } + + long lastModified = latestLog.lastModified(); + if(lastModified < System.currentTimeMillis() - 1000 * 60) { + LOGGER.debug("latest.log at " + latestLog.getAbsolutePath() + " is too old (timestamp: " + lastModified + ")"); + return null; + } + + try(InputStream is = new FileInputStream(latestLog)) { + List lines = readLines(is); + if(lines.stream().anyMatch(l -> l.contains(cookie))) { + return lines; + } else { + LOGGER.debug("Failed to find magic cookie in latest.log at " + latestLog.getAbsolutePath()); + } + } catch (IOException e) { + LOGGER.error("Failed to read latest.log at " + latestLog.getAbsolutePath(), e); + } + + return null; + } + + private static void setLog4jConfig(URI uri) { + for (LoggerContext context : getLoggerContexts()) { + context.setConfigLocation(uri); + context.reconfigure(); + } + } + + private static Collection getLoggerContexts() { + LoggerContextFactory loggerContextFactory = LogManager.getFactory(); + if (loggerContextFactory instanceof Log4jContextFactory) { + return ((Log4jContextFactory) loggerContextFactory).getSelector().getLoggerContexts(); + } else { + throw new IllegalStateException("Logger context factory is not a Log4jContextFactory"); + } + } + + private static URI findForgeLog4j() { + try { + List candidates = Collections.list(NotEnoughVerbosity.class.getClassLoader().getResources("log4j2.xml")); + + LOGGER.info("Logger configs on classpath: " + candidates); + + for(URL url : candidates) { + try { + if(readLines(url.openStream()).stream().anyMatch(l -> l.contains("Root level=\"all\""))) { + return url.toURI(); + } + } catch(Exception e) { + LOGGER.error("Failed to read inspect logger config at URL " + url, e); + } + } + } catch(Exception e) { + LOGGER.error("Exception enumerating logger configs", e); + } + return null; + } + + private static List readLines(InputStream stream) { + return new BufferedReader(new InputStreamReader(stream)).lines().collect(Collectors.toList()); + } +}