diff --git a/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java b/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java index d635f2a2..5bc59ce3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java +++ b/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java @@ -28,6 +28,7 @@ import java.io.File; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -335,6 +336,11 @@ public class MixinEnvironment { */ private final boolean[] options; + /** + * Error handlers for environment + */ + private final Set errorHandlers = new LinkedHashSet(); + /** * Detected side */ @@ -352,7 +358,7 @@ public class MixinEnvironment { * Class name transformer (if present) */ private IClassNameTransformer nameTransformer; - + private MixinEnvironment(Phase phase) { this.phase = phase; this.configsKey = MixinEnvironment.CONFIGS_KEY + "." + this.phase.name.toLowerCase(); @@ -421,6 +427,24 @@ public class MixinEnvironment { } return this; } + + /** + * Add a new error handler class to this environment + * + * @param handlerName Handler class to add + * @return fluent interface + */ + public MixinEnvironment registerErrorHandlerClass(String handlerName) { + this.errorHandlers.add(handlerName); + return this; + } + + /** + * Get all registered error handlers for this environment + */ + public Set getErrorHandlerClasses() { + return Collections.unmodifiableSet(this.errorHandlers); + } /** * Get the active mixin transformer instance (if any) diff --git a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java new file mode 100644 index 00000000..b46dfed2 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java @@ -0,0 +1,63 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered.org + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.asm.mixin.extensibility; + +import java.util.Set; + +public interface IMixinConfig { + + /** + * Get the name of the file from which this configuration object was + * initialised + */ + public abstract String getName(); + + /** + * Get the package containing all mixin classes + */ + public abstract String getMixinPackage(); + + /** + * Get the priority + */ + public abstract int getPriority(); + + /** + * Get the companion plugin, if available + */ + public abstract IMixinConfigPlugin getPlugin(); + + /** + * True if this mixin is required (failure to apply a defined mixin + * is an error condition). + */ + public abstract boolean isRequired(); + + /** + * Get targets for this configuration + */ + public abstract Set getTargets(); + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinErrorHandler.java b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinErrorHandler.java new file mode 100644 index 00000000..15c558c5 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinErrorHandler.java @@ -0,0 +1,93 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered.org + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.asm.mixin.extensibility; + +import org.apache.logging.log4j.Level; + + +/** + * Interface for objects which want to perform custom behaviour when fatal mixin + * errors occur. For example displaying a user-friendly error message + */ +public interface IMixinErrorHandler { + + /** + * Action to take when handling an error. By default, if a config is marked + * as "required" then the default action will be {@link #ERROR}, and will be + * {@link #WARN} otherwise. + */ + public static enum ErrorAction { + + /** + * Take no action, this should be treated as a non-critical error and + * processing should continue + */ + NONE(Level.INFO), + + /** + * Generate a warning but continue processing + */ + WARN(Level.WARN), + + /** + * Throw a {@link org.spongepowered.asm.mixin.MixinApplyError} to halt + * further processing if possible + */ + ERROR(Level.FATAL); + + public final Level logLevel; + + private ErrorAction(Level logLevel) { + this.logLevel = logLevel; + } + } + + /** + * Called when an error occurs applying a mixin. This allows + * the plugin to display more user-friendly error messages if required. + * + *

By default, when a critical error occurs the mixin processor will + * raise a warning if the config is not marked as "required" and will throw + * an {@link Error} if it is. This behaviour can be altered by returning + * different values from this method.

+ * + *

The original throwable which was caught is passed in via the + * th parameter and the default action is passed in to the + * action parameter. A plugin can choose to output a friendly message + * but leave the original behaviour intact (by returning null + * or returning action directly. Alternatively it may throw a + * different exception or error, or can reduce the severity of the error by + * returning a different {@link ErrorAction}.

+ * + * @param targetClassName Class being transformed when the error occurred + * @param th Throwable which was caught + * @param mixin Mixin which was being applied at the time of the error + * @param action Default action + * @return null to perform the default action (or return action) or new + * action to take + */ + public abstract ErrorAction onError(String targetClassName, Throwable th, IMixinInfo mixin, ErrorAction action); + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinInfo.java b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinInfo.java index caf8a925..df57f5cc 100644 --- a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinInfo.java @@ -27,9 +27,17 @@ package org.spongepowered.asm.mixin.extensibility; import java.util.List; import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.MixinEnvironment.Phase; public interface IMixinInfo { + /** + * Get the config to which this mixin belongs + * + * @return the mixin config + */ + public IMixinConfig getConfig(); + /** * Get the simple name of the mixin * @@ -42,7 +50,7 @@ public interface IMixinInfo { * * @return mixin class name */ - public abstract String getClassName(); + public abstract String getClassName(); /** * Get the ref (internal name) of the mixin class @@ -87,4 +95,11 @@ public interface IMixinInfo { * @return the priority */ public abstract int getPriority(); + + /** + * Get the mixin phase + * + * @return the phase + */ + public abstract Phase getPhase(); } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java index f8464e03..ac7a7b5f 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java @@ -43,6 +43,7 @@ import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.launch.MixinInitialisationError; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.extensibility.IMixinConfig; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.injection.struct.ReferenceMapper; import org.spongepowered.asm.util.VersionNumber; @@ -53,7 +54,7 @@ import com.google.gson.annotations.SerializedName; /** * Mixin configuration bundle */ -class MixinConfig implements Comparable { +class MixinConfig implements Comparable, IMixinConfig { /** * Global order of mixin configs, used to determine ordering between configs @@ -350,19 +351,19 @@ class MixinConfig implements Comparable { void postApply(String transformedName, ClassNode targetClass) { this.unhandledTargets.remove(transformedName); } - - /** - * True if this mixin is required (failure to apply a defined mixin - * is an error condition). + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.IMixinConfig#isRequired() */ + @Override public boolean isRequired() { return this.required; } - /** - * Get the name of the file from which this configuration object was - * initialised + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.IMixinConfig#getName() */ + @Override public String getName() { return this.name; } @@ -370,10 +371,19 @@ class MixinConfig implements Comparable { /** * Get the package containing all mixin classes */ + @Override public String getMixinPackage() { return this.mixinPackage; } + /** + * Get the priority + */ + @Override + public int getPriority() { + return this.priority; + } + /** * Get the list of mixin classes we will be applying */ @@ -396,16 +406,18 @@ class MixinConfig implements Comparable { return this.refMapper; } - /** - * Get the companion plugin, if available + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.IMixinConfig#getPlugin() */ + @Override public IMixinConfigPlugin getPlugin() { return this.plugin; } - /** - * Get targets for this configuration + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.IMixinConfig#getTargets() */ + @Override public Set getTargets() { return Collections.unmodifiableSet(this.mixinMapping.keySet()); } @@ -494,7 +506,7 @@ class MixinConfig implements Comparable { } return (this.priority - other.priority); } - + /** * Factory method, creates a new mixin configuration bundle from the * specified configFile, which must be accessible on the classpath diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java index f1693f65..b814f79c 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java @@ -52,6 +52,7 @@ import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Phase; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.extensibility.IMixinConfig; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.util.ASMHelper; @@ -371,6 +372,14 @@ class MixinInfo extends TreeInfo implements Comparable, IMixinInfo { ClassInfo getClassInfo() { return this.classInfo; } + + /** + * Get the parent config which declares this mixin + */ + @Override + public IMixinConfig getConfig() { + return this.parent; + } /** * Get the parent config which declares this mixin @@ -430,6 +439,7 @@ class MixinInfo extends TreeInfo implements Comparable, IMixinInfo { /** * Get the phase in which this mixin was initialised */ + @Override public Phase getPhase() { return this.phase; } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java index 0f5cce2a..c45342f4 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java @@ -51,6 +51,8 @@ import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.MixinEnvironment.Phase; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinErrorHandler; +import org.spongepowered.asm.mixin.extensibility.IMixinErrorHandler.ErrorAction; import org.spongepowered.asm.transformers.TreeTransformer; import org.spongepowered.asm.util.Constants; @@ -253,7 +255,12 @@ public class MixinTransformer extends TreeTransformer { /** * Logging level for verbose messages */ - private Level verboseLoggingLevel = Level.DEBUG; + private Level verboseLoggingLevel = Level.DEBUG; + + /** + * Handling an error state, do not process further mixins + */ + private boolean errorState = false; /** * ctor @@ -298,7 +305,7 @@ public class MixinTransformer extends TreeTransformer { */ @Override public byte[] transform(String name, String transformedName, byte[] basicClass) { - if (basicClass == null || transformedName == null) { + if (basicClass == null || transformedName == null || this.errorState) { return basicClass; } @@ -356,17 +363,7 @@ public class MixinTransformer extends TreeTransformer { MixinTransformer.dumpClass(transformedName.replace('.', '/') + ".target", basicClass); } - MixinInfo mixin = th.getMixin(); - Phase phase = mixin.getPhase(); - MixinConfig config = mixin.getParent(); - this.logger.log(config.isRequired() ? Level.FATAL : Level.WARN, String.format("Mixin failed applying %s -> %s: %s %s", - mixin, transformedName, th.getClass().getName(), th.getMessage()), th); - - if (config.isRequired()) { - throw new MixinApplyError("Mixin [" + mixin + "] from " + phase + " FAILED for REQUIRED config [" + config + "]", th); - } - - th.printStackTrace(); + this.handleMixinErrorState(transformedName, th); } } @@ -564,6 +561,50 @@ public class MixinTransformer extends TreeTransformer { } } + private void handleMixinErrorState(String targetClassName, InvalidMixinException th) throws MixinApplyError { + this.errorState = true; + + MixinInfo mixin = th.getMixin(); + MixinConfig config = mixin.getParent(); + ErrorAction action = config.isRequired() ? ErrorAction.ERROR : ErrorAction.WARN; + + for (IMixinErrorHandler handler : this.getErrorHandlers(mixin.getPhase())) { + ErrorAction newAction = handler.onError(targetClassName, th, mixin, action); + if (newAction != null) { + action = newAction; + } + } + + this.logger.log(action.logLevel, String.format("Mixin failed applying %s -> %s: %s %s", mixin, targetClassName, + th.getClass().getName(), th.getMessage()), th); + + this.errorState = false; + + if (action == ErrorAction.ERROR) { + throw new MixinApplyError(String.format("Mixin [%s] from phase [%s] in config [%s] FAILED", mixin, mixin.getPhase(), + config), th); + } + } + + private List getErrorHandlers(Phase phase) { + List handlers = new ArrayList(); + + for (String handlerClassName : MixinEnvironment.getEnvironment(phase).getErrorHandlerClasses()) { + try { + this.logger.info("Instancing error handler class {}", handlerClassName); + Class handlerClass = Class.forName(handlerClassName, true, Launch.classLoader); + IMixinErrorHandler handler = (IMixinErrorHandler)handlerClass.newInstance(); + if (handler != null) { + handlers.add(handler); + } + } catch (Throwable th) { + // skip bad handlers + } + } + + return handlers; + } + private byte[] writeClass(String transformedName, ClassNode targetClass) { // Collapse tree to bytes byte[] bytes = this.writeClass(targetClass); diff --git a/src/main/java/org/spongepowered/asm/util/PrettyPrinter.java b/src/main/java/org/spongepowered/asm/util/PrettyPrinter.java index c812ea87..6810c7bb 100644 --- a/src/main/java/org/spongepowered/asm/util/PrettyPrinter.java +++ b/src/main/java/org/spongepowered/asm/util/PrettyPrinter.java @@ -28,6 +28,9 @@ import java.io.PrintStream; import java.util.ArrayList; import java.util.List; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; + import com.google.common.base.Strings; @@ -89,6 +92,26 @@ public class PrettyPrinter { return this; } + /** + * @param array + * @return + */ + public PrettyPrinter add(Object[] array) { + return this.add(array, "%s"); + } + + /** + * @param array + * @return + */ + public PrettyPrinter add(Object[] array, String format) { + for (Object element : array) { + this.add(format, element); + } + + return this; + } + /** * Adds a horizontal rule to the output * @@ -147,4 +170,35 @@ public class PrettyPrinter { private void printHr(PrintStream stream, char... hrChars) { stream.printf("/*%s*/\n", Strings.repeat(new String(hrChars), this.width + 2)); } + + public void log(Logger logger) { + this.log(logger, Level.INFO); + } + + /** + * Write this printer to the specified logger + * + * @param logger logger to log to + * @param level log level + */ + public void log(Logger logger, Level level) { + this.logHr(logger, level, '*'); + for (String line : this.lines) { + int len = line.length(); + if (len > 1 && line.charAt(0) == PrettyPrinter.HR) { + this.logHr(logger, level, line.charAt(1)); + } else { + if (len > 0 && line.charAt(0) == PrettyPrinter.CENTRE) { + String text = line.substring(1); + line = String.format("%" + (((this.width - (text.length())) / 2) + text.length()) + "s", text); + } + logger.log(level, String.format("/* %-" + this.width + "s */", line)); + } + } + this.logHr(logger, level, '*'); + } + + private void logHr(Logger logger, Level level, char... hrChars) { + logger.log(level, String.format("/*%s*/", Strings.repeat(new String(hrChars), this.width + 2))); + } }