Add error handler support

This commit is contained in:
Mumfrey committed 2015-04-28 17:50:19 +01:00
1 parent 88fafc87e5
commit dffb1065d3
8 files changed
+340 -28

No files matched your search

@@ -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<String> errorHandlers = new LinkedHashSet<String>();
/**
* 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<String> getErrorHandlerClasses() {
return Collections.<String>unmodifiableSet(this.errorHandlers);
}
/**
* Get the active mixin transformer instance (if any)
@@ -0,0 +1,63 @@
/*
* This file is part of Sponge, licensed under the MIT License (MIT).
*
* Copyright (c) SpongePowered.org <http://www.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 <em>required</em> (failure to apply a defined mixin
* is an <em>error</em> condition).
*/
public abstract boolean isRequired();
/**
* Get targets for this configuration
*/
public abstract Set<String> getTargets();
}
@@ -0,0 +1,93 @@
/*
* This file is part of Sponge, licensed under the MIT License (MIT).
*
* Copyright (c) SpongePowered.org <http://www.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.
*
* <p>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.</p>
*
* <p>The original throwable which was caught is passed in via the <code>
* th</code> parameter and the default action is passed in to the <code>
* action</code> parameter. A plugin can choose to output a friendly message
* but leave the original behaviour intact (by returning <code>null</code>
* or returning <code>action</code> directly. Alternatively it may throw a
* different exception or error, or can reduce the severity of the error by
* returning a different {@link ErrorAction}.</p>
*
* @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);
}
@@ -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();
}
@@ -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<MixinConfig> {
class MixinConfig implements Comparable<MixinConfig>, IMixinConfig {
/**
* Global order of mixin configs, used to determine ordering between configs
@@ -350,19 +351,19 @@ class MixinConfig implements Comparable<MixinConfig> {
void postApply(String transformedName, ClassNode targetClass) {
this.unhandledTargets.remove(transformedName);
}
/**
* True if this mixin is <em>required</em> (failure to apply a defined mixin
* is an <em>error</em> 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<MixinConfig> {
/**
* 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<MixinConfig> {
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<String> getTargets() {
return Collections.<String>unmodifiableSet(this.mixinMapping.keySet());
}
@@ -494,7 +506,7 @@ class MixinConfig implements Comparable<MixinConfig> {
}
return (this.priority - other.priority);
}
/**
* Factory method, creates a new mixin configuration bundle from the
* specified configFile, which must be accessible on the classpath
@@ -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<MixinInfo>, 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<MixinInfo>, IMixinInfo {
/**
* Get the phase in which this mixin was initialised
*/
@Override
public Phase getPhase() {
return this.phase;
}
@@ -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<IMixinErrorHandler> getErrorHandlers(Phase phase) {
List<IMixinErrorHandler> handlers = new ArrayList<IMixinErrorHandler>();
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);
@@ -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)));
}
}