ClassTransformerManager.java 7.26 KB
/*
 * This file is part of LiteLoader.
 * Copyright (C) 2012-16 Adam Mummery-Smith
 * All Rights Reserved.
 */
package com.mumfrey.liteloader.launch;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;

import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity;

import net.minecraft.launchwrapper.IClassTransformer;
import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraft.launchwrapper.LogWrapper;

/**
 * Manages injection of required and optional transformers
 * 
 * @author Adam Mummery-Smith
 */
public class ClassTransformerManager
{
    /**
     * Once the game is started we can no longer inject transformers
     */
    private boolean gameStarted;

    /**
     * Transformers to inject after preInit but before the game starts,
     * necessary for anything that needs to be downstream of forge.
     */
    private Set<String> downstreamTransformers = new LinkedHashSet<String>();

    /**
     * Transformers passed into the constructor which are required and must be
     * injected upstream.
     */
    private final List<String> requiredTransformers;

    /**
     * Transformers successfully injected by us
     */
    private final Set<String> injectedTransformers = new LinkedHashSet<String>();

    /**
     * Catalogue of transformer startup failures
     */
    private final Map<String, List<Throwable>> transformerStartupErrors = new HashMap<String, List<Throwable>>();

    private Logger attachedLog;

    private String pendingTransformer;

    class ThrowableObserver extends AbstractAppender
    {
        public ThrowableObserver()
        {
            super("Throwable Observer", null, null);
            this.start();
        }

        @Override
        public void append(LogEvent event)
        {
            ClassTransformerManager.this.observeThrowable(event.getThrown());
        }
    }

    /**
     * @param requiredTransformers
     */
    public ClassTransformerManager(List<String> requiredTransformers)
    {
        this.requiredTransformers = requiredTransformers;

        this.appendObserver();
    }

    private void appendObserver()
    {
        try
        {
            Field fLogger = LogWrapper.class.getDeclaredField("myLog");
            fLogger.setAccessible(true);
            this.attachedLog = (Logger)fLogger.get(LogWrapper.log);
            if (this.attachedLog instanceof org.apache.logging.log4j.core.Logger)
            {
                ((org.apache.logging.log4j.core.Logger)this.attachedLog).addAppender(new ThrowableObserver());
            }
        }
        catch (Exception ex)
        {
            LiteLoaderLogger.warning("Failed to append ThrowableObserver to LogWrapper, transformer startup exceptions may not be logged");
        }
    }

    /**
     * @param transformerClass
     */
    public boolean injectTransformer(String transformerClass)
    {
        if (!this.gameStarted)
        {
            this.downstreamTransformers.add(transformerClass);
            return true;
        }

        return false;
    }

    /**
     * @param transformerClasses
     */
    public boolean injectTransformers(Collection<String> transformerClasses)
    {
        if (!this.gameStarted)
        {
            this.downstreamTransformers.addAll(transformerClasses);
            return true;
        }

        return false;
    }

    /**
     * @param transformerClasses
     */
    public boolean injectTransformers(String[] transformerClasses)
    {
        if (!this.gameStarted)
        {
            this.downstreamTransformers.addAll(Arrays.asList(transformerClasses));
            return true;
        }

        return false;
    }

    /**
     * @param classLoader
     */
    void injectUpstreamTransformers(LaunchClassLoader classLoader)
    {
        for (String requiredTransformerClassName : this.requiredTransformers)
        {
            LiteLoaderLogger.info(Verbosity.REDUCED, "Injecting required class transformer '%s'", requiredTransformerClassName);
            this.injectTransformer(classLoader, requiredTransformerClassName);
        }
    }

    /**
     * @param classLoader
     */
    void injectDownstreamTransformers(LaunchClassLoader classLoader)
    {
        this.gameStarted = true;

        if (this.downstreamTransformers.size() > 0)
        {
            LiteLoaderLogger.info("Injecting downstream transformers");
        }

        for (String transformerClassName : this.downstreamTransformers)
        {
            LiteLoaderLogger.info(Verbosity.REDUCED, "Injecting additional class transformer class '%s'", transformerClassName);
            this.injectTransformer(classLoader, transformerClassName);
        }

        this.downstreamTransformers.clear();
    }

    private synchronized void injectTransformer(LaunchClassLoader classLoader, String transformerClassName)
    {
        try
        {
            // Assign pendingTransformer so that logged errors during transformer init can be put in the map
            this.pendingTransformer = transformerClassName;

            // Register the transformer
            classLoader.registerTransformer(transformerClassName);

            // Unassign pending transformer now init is completed
            this.pendingTransformer = null;

            // Check whether the transformer was successfully injected, look for it in the transformer list
            if (this.findTransformer(classLoader, transformerClassName) != null)
            {
                this.injectedTransformers.add(transformerClassName);
            }
        }
        catch (Throwable th)
        {
            LiteLoaderLogger.severe(th, "Error injecting class transformer class %s", transformerClassName);
        }
    }

    public void observeThrowable(Throwable th)
    {
        if (th != null && this.pendingTransformer != null)
        {
            List<Throwable> transformerErrors = this.transformerStartupErrors.get(this.pendingTransformer);
            if (transformerErrors == null)
            {
                transformerErrors = new ArrayList<Throwable>();
                this.transformerStartupErrors.put(this.pendingTransformer, transformerErrors);
            }
            transformerErrors.add(th);
        }
    }

    private IClassTransformer findTransformer(LaunchClassLoader classLoader, String transformerClassName)
    {
        for (IClassTransformer transformer : classLoader.getTransformers())
        {
            if (transformer.getClass().getName().equals(transformerClassName))
            {
                return transformer;
            }
        }

        return null;
    }

    public Set<String> getInjectedTransformers()
    {
        return Collections.unmodifiableSet(this.injectedTransformers);
    }

    public List<Throwable> getTransformerStartupErrors(String transformerClassName)
    {
        List<Throwable> errorList = this.transformerStartupErrors.get(transformerClassName);
        return errorList != null ? Collections.unmodifiableList(errorList) : null;
    }
}