    001package cpw.mods.fml.relauncher;
    002
    003import java.io.File;
    004import java.io.FileInputStream;
    005import java.io.FileOutputStream;
    006import java.io.FilenameFilter;
    007import java.io.IOException;
    008import java.io.InputStream;
    009import java.io.InterruptedIOException;
    010import java.lang.reflect.Method;
    011import java.net.MalformedURLException;
    012import java.net.URL;
    013import java.net.URLConnection;
    014import java.nio.ByteBuffer;
    015import java.nio.MappedByteBuffer;
    016import java.nio.channels.FileChannel;
    017import java.nio.channels.FileChannel.MapMode;
    018import java.security.MessageDigest;
    019import java.util.ArrayList;
    020import java.util.Arrays;
    021import java.util.HashMap;
    022import java.util.List;
    023import java.util.Map;
    024import java.util.jar.Attributes;
    025import java.util.jar.JarFile;
    026import java.util.logging.Level;
    027
    028import cpw.mods.fml.common.CertificateHelper;
    029import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions;
    030
    031public class RelaunchLibraryManager
    032{
    033    private static String[] rootPlugins =  { "cpw.mods.fml.relauncher.FMLCorePlugin" , "net.minecraftforge.classloading.FMLForgePlugin" };
    034    private static List<String> loadedLibraries = new ArrayList<String>();
    035    private static Map<IFMLLoadingPlugin, File> pluginLocations;
    036    private static List<IFMLLoadingPlugin> loadPlugins;
    037    private static List<ILibrarySet> libraries;
    038    public static void handleLaunch(File mcDir, RelaunchClassLoader actualClassLoader)
    039    {
    040        pluginLocations = new HashMap<IFMLLoadingPlugin, File>();
    041        loadPlugins = new ArrayList<IFMLLoadingPlugin>();
    042        libraries = new ArrayList<ILibrarySet>();
    043        for (String s : rootPlugins)
    044        {
    045            try
    046            {
    047                IFMLLoadingPlugin plugin = (IFMLLoadingPlugin) Class.forName(s, true, actualClassLoader).newInstance();
    048                loadPlugins.add(plugin);
    049                for (String libName : plugin.getLibraryRequestClass())
    050                {
    051                    libraries.add((ILibrarySet) Class.forName(libName, true, actualClassLoader).newInstance());
    052                }
    053            }
    054            catch (Exception e)
    055            {
    056                // HMMM
    057            }
    058        }
    059
    060        if (loadPlugins.isEmpty())
    061        {
    062            throw new RuntimeException("A fatal error has occured - no valid fml load plugin was found - this is a completely corrupt FML installation.");
    063        }
    064
    065        downloadMonitor.updateProgressString("All core mods are successfully located");
    066        // Now that we have the root plugins loaded - lets see what else might be around
    067        String commandLineCoremods = System.getProperty("fml.coreMods.load","");
    068        for (String s : commandLineCoremods.split(","))
    069        {
    070            if (s.isEmpty())
    071            {
    072                continue;
    073            }
    074            FMLRelaunchLog.info("Found a command line coremod : %s", s);
    075            try
    076            {
    077                actualClassLoader.addTransformerExclusion(s);
    078                Class<?> coreModClass = Class.forName(s, true, actualClassLoader);
    079                TransformerExclusions trExclusions = coreModClass.getAnnotation(IFMLLoadingPlugin.TransformerExclusions.class);
    080                if (trExclusions!=null)
    081                {
    082                    for (String st : trExclusions.value())
    083                    {
    084                        actualClassLoader.addTransformerExclusion(st);
    085                    }
    086                }
    087                IFMLLoadingPlugin plugin = (IFMLLoadingPlugin) coreModClass.newInstance();
    088                loadPlugins.add(plugin);
    089                if (plugin.getLibraryRequestClass()!=null)
    090                {
    091                    for (String libName : plugin.getLibraryRequestClass())
    092                    {
    093                        libraries.add((ILibrarySet) Class.forName(libName, true, actualClassLoader).newInstance());
    094                    }
    095                }
    096            }
    097            catch (Throwable e)
    098            {
    099                FMLRelaunchLog.log(Level.SEVERE,e,"Exception occured trying to load coremod %s",s);
    100                throw new RuntimeException(e);
    101            }
    102        }
    103        discoverCoreMods(mcDir, actualClassLoader, loadPlugins, libraries);
    104
    105        List<Throwable> caughtErrors = new ArrayList<Throwable>();
    106        try
    107        {
    108            File libDir;
    109            try
    110            {
    111                libDir = setupLibDir(mcDir);
    112            }
    113            catch (Exception e)
    114            {
    115                caughtErrors.add(e);
    116                return;
    117            }
    118
    119            for (ILibrarySet lib : libraries)
    120            {
    121                for (int i=0; i<lib.getLibraries().length; i++)
    122                {
    123                    boolean download = false;
    124                    String libName = lib.getLibraries()[i];
    125                    String targFileName = libName.lastIndexOf('/')>=0 ? libName.substring(libName.lastIndexOf('/')) : libName;
    126                    String checksum = lib.getHashes()[i];
    127                    File libFile = new File(libDir, targFileName);
    128                    if (!libFile.exists())
    129                    {
    130                        try
    131                        {
    132                            downloadFile(libFile, lib.getRootURL(), libName, checksum);
    133                            download = true;
    134                        }
    135                        catch (Throwable e)
    136                        {
    137                            caughtErrors.add(e);
    138                            continue;
    139                        }
    140                    }
    141
    142                    if (libFile.exists() && !libFile.isFile())
    143                    {
    144                        caughtErrors.add(new RuntimeException(String.format("Found a file %s that is not a normal file - you should clear this out of the way", libName)));
    145                        continue;
    146                    }
    147
    148                    if (!download)
    149                    {
    150                        try
    151                        {
    152                            FileInputStream fis = new FileInputStream(libFile);
    153                            FileChannel chan = fis.getChannel();
    154                            MappedByteBuffer mappedFile = chan.map(MapMode.READ_ONLY, 0, libFile.length());
    155                            String fileChecksum = generateChecksum(mappedFile);
    156                            fis.close();
    157                            // bad checksum and I did not download this file
    158                            if (!checksum.equals(fileChecksum))
    159                            {
    160                                caughtErrors.add(new RuntimeException(String.format("The file %s was found in your lib directory and has an invalid checksum %s (expecting %s) - it is unlikely to be the correct download, please move it out of the way and try again.", libName, fileChecksum, checksum)));
    161                                continue;
    162                            }
    163                        }
    164                        catch (Exception e)
    165                        {
    166                            FMLRelaunchLog.log(Level.SEVERE, e, "The library file %s could not be validated", libFile.getName());
    167                            caughtErrors.add(new RuntimeException(String.format("The library file %s could not be validated", libFile.getName()),e));
    168                            continue;
    169                        }
    170                    }
    171
    172                    if (!download)
    173                    {
    174                        downloadMonitor.updateProgressString("Found library file %s present and correct in lib dir", libName);
    175                    }
    176                    else
    177                    {
    178                        downloadMonitor.updateProgressString("Library file %s was downloaded and verified successfully", libName);
    179                    }
    180
    181                    try
    182                    {
    183                        actualClassLoader.addURL(libFile.toURI().toURL());
    184                        loadedLibraries.add(libName);
    185                    }
    186                    catch (MalformedURLException e)
    187                    {
    188                        caughtErrors.add(new RuntimeException(String.format("Should never happen - %s is broken - probably a somehow corrupted download. Delete it and try again.", libFile.getName()), e));
    189                    }
    190                }
    191            }
    192        }
    193        finally
    194        {
    195            if (downloadMonitor.shouldStopIt())
    196            {
    197                return;
    198            }
    199            if (!caughtErrors.isEmpty())
    200            {
    201                FMLRelaunchLog.severe("There were errors during initial FML setup. " +
    202                        "Some files failed to download or were otherwise corrupted. " +
    203                        "You will need to manually obtain the following files from " +
    204                        "these download links and ensure your lib directory is clean. ");
    205                for (ILibrarySet set : libraries)
    206                {
    207                    for (String file : set.getLibraries())
    208                    {
    209                        FMLRelaunchLog.severe("*** Download "+set.getRootURL(), file);
    210                    }
    211                }
    212                FMLRelaunchLog.severe("<===========>");
    213                FMLRelaunchLog.severe("The following is the errors that caused the setup to fail. " +
    214                        "They may help you diagnose and resolve the issue");
    215                for (Throwable t : caughtErrors)
    216                {
    217                    if (t.getMessage()!=null)
    218                    {
    219                        FMLRelaunchLog.severe(t.getMessage());
    220                    }
    221                }
    222                FMLRelaunchLog.severe("<<< ==== >>>");
    223                FMLRelaunchLog.severe("The following is diagnostic information for developers to review.");
    224                for (Throwable t : caughtErrors)
    225                {
    226                    FMLRelaunchLog.log(Level.SEVERE, t, "Error details");
    227                }
    228                throw new RuntimeException("A fatal error occured and FML cannot continue");
    229            }
    230        }
    231
    232        for (IFMLLoadingPlugin plug : loadPlugins)
    233        {
    234            if (plug.getASMTransformerClass()!=null)
    235            {
    236                for (String xformClass : plug.getASMTransformerClass())
    237                {
    238                    actualClassLoader.registerTransformer(xformClass);
    239                }
    240            }
    241        }
    242
    243        downloadMonitor.updateProgressString("Running coremod plugins");
    244        Map<String,Object> data = new HashMap<String,Object>();
    245        data.put("mcLocation", mcDir);
    246        data.put("coremodList", loadPlugins);
    247        for (IFMLLoadingPlugin plugin : loadPlugins)
    248        {
    249            downloadMonitor.updateProgressString("Running coremod plugin %s", plugin.getClass().getSimpleName());
    250            data.put("coremodLocation", pluginLocations.get(plugin));
    251            plugin.injectData(data);
    252            String setupClass = plugin.getSetupClass();
    253            if (setupClass != null)
    254            {
    255                try
    256                {
    257                    IFMLCallHook call = (IFMLCallHook) Class.forName(setupClass, true, actualClassLoader).newInstance();
    258                    Map<String,Object> callData = new HashMap<String, Object>();
    259                    callData.put("classLoader", actualClassLoader);
    260                    call.injectData(callData);
    261                    call.call();
    262                }
    263                catch (Exception e)
    264                {
    265                    throw new RuntimeException(e);
    266                }
    267            }
    268            downloadMonitor.updateProgressString("Coremod plugin %s run successfully", plugin.getClass().getSimpleName());
    269
    270            String modContainer = plugin.getModContainerClass();
    271            if (modContainer != null)
    272            {
    273                FMLInjectionData.containers.add(modContainer);
    274            }
    275        }
    276        try
    277        {
    278            downloadMonitor.updateProgressString("Validating minecraft");
    279            Class<?> loaderClazz = Class.forName("cpw.mods.fml.common.Loader", true, actualClassLoader);
    280            Method m = loaderClazz.getMethod("injectData", Object[].class);
    281            m.invoke(null, (Object)FMLInjectionData.data());
    282            m = loaderClazz.getMethod("instance");
    283            m.invoke(null);
    284            downloadMonitor.updateProgressString("Minecraft validated, launching...");
    285            downloadBuffer = null;
    286        }
    287        catch (Exception e)
    288        {
    289            // Load in the Loader, make sure he's ready to roll - this will initialize most of the rest of minecraft here
    290            System.out.println("A CRITICAL PROBLEM OCCURED INITIALIZING MINECRAFT - LIKELY YOU HAVE AN INCORRECT VERSION FOR THIS FML");
    291            throw new RuntimeException(e);
    292        }
    293    }
    294
    295    private static void discoverCoreMods(File mcDir, RelaunchClassLoader classLoader, List<IFMLLoadingPlugin> loadPlugins, List<ILibrarySet> libraries)
    296    {
    297        downloadMonitor.updateProgressString("Discovering coremods");
    298        File coreMods = setupCoreModDir(mcDir);
    299        FilenameFilter ff = new FilenameFilter()
    300        {
    301            @Override
    302            public boolean accept(File dir, String name)
    303            {
    304                return name.endsWith(".jar");
    305            }
    306        };
    307        File[] coreModList = coreMods.listFiles(ff);
    308        Arrays.sort(coreModList);
    309
    310        for (File coreMod : coreModList)
    311        {
    312            downloadMonitor.updateProgressString("Found a candidate coremod %s", coreMod.getName());
    313            JarFile jar;
    314            Attributes mfAttributes;
    315            try
    316            {
    317                jar = new JarFile(coreMod);
    318                mfAttributes = jar.getManifest().getMainAttributes();
    319            }
    320            catch (IOException ioe)
    321            {
    322                FMLRelaunchLog.log(Level.SEVERE, ioe, "Unable to read the coremod jar file %s - ignoring", coreMod.getName());
    323                continue;
    324            }
    325
    326            String fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin");
    327            if (fmlCorePlugin == null)
    328            {
    329                FMLRelaunchLog.severe("The coremod %s does not contain a valid jar manifest- it will be ignored", coreMod.getName());
    330                continue;
    331            }
    332
    333//            String className = fmlCorePlugin.replace('.', '/').concat(".class");
    334//            JarEntry ent = jar.getJarEntry(className);
    335//            if (ent ==null)
    336//            {
    337//                FMLLog.severe("The coremod %s specified %s as it's loading class but it does not include it - it will be ignored", coreMod.getName(), fmlCorePlugin);
    338//                continue;
    339//            }
    340//            try
    341//            {
    342//                Class<?> coreModClass = Class.forName(fmlCorePlugin, false, classLoader);
    343//                FMLLog.severe("The coremods %s specified a class %s that is already present in the classpath - it will be ignored", coreMod.getName(), fmlCorePlugin);
    344//                continue;
    345//            }
    346//            catch (ClassNotFoundException cnfe)
    347//            {
    348//                // didn't find it, good
    349//            }
    350            try
    351            {
    352                classLoader.addURL(coreMod.toURI().toURL());
    353            }
    354            catch (MalformedURLException e)
    355            {
    356                FMLRelaunchLog.log(Level.SEVERE, e, "Unable to convert file into a URL. weird");
    357                continue;
    358            }
    359            try
    360            {
    361                downloadMonitor.updateProgressString("Loading coremod %s", coreMod.getName());
    362                classLoader.addTransformerExclusion(fmlCorePlugin);
    363                Class<?> coreModClass = Class.forName(fmlCorePlugin, true, classLoader);
    364                TransformerExclusions trExclusions = coreModClass.getAnnotation(IFMLLoadingPlugin.TransformerExclusions.class);
    365                if (trExclusions!=null)
    366                {
    367                    for (String st : trExclusions.value())
    368                    {
    369                        classLoader.addTransformerExclusion(st);
    370                    }
    371                }
    372                IFMLLoadingPlugin plugin = (IFMLLoadingPlugin) coreModClass.newInstance();
    373                loadPlugins.add(plugin);
    374                pluginLocations .put(plugin, coreMod);
    375                if (plugin.getLibraryRequestClass()!=null)
    376                {
    377                    for (String libName : plugin.getLibraryRequestClass())
    378                    {
    379                        libraries.add((ILibrarySet) Class.forName(libName, true, classLoader).newInstance());
    380                    }
    381                }
    382                downloadMonitor.updateProgressString("Loaded coremod %s", coreMod.getName());
    383            }
    384            catch (ClassNotFoundException cnfe)
    385            {
    386                FMLRelaunchLog.log(Level.SEVERE, cnfe, "Coremod %s: Unable to class load the plugin %s", coreMod.getName(), fmlCorePlugin);
    387            }
    388            catch (ClassCastException cce)
    389            {
    390                FMLRelaunchLog.log(Level.SEVERE, cce, "Coremod %s: The plugin %s is not an implementor of IFMLLoadingPlugin", coreMod.getName(), fmlCorePlugin);
    391            }
    392            catch (InstantiationException ie)
    393            {
    394                FMLRelaunchLog.log(Level.SEVERE, ie, "Coremod %s: The plugin class %s was not instantiable", coreMod.getName(), fmlCorePlugin);
    395            }
    396            catch (IllegalAccessException iae)
    397            {
    398                FMLRelaunchLog.log(Level.SEVERE, iae, "Coremod %s: The plugin class %s was not accessible", coreMod.getName(), fmlCorePlugin);
    399            }
    400        }
    401    }
    402
    403    /**
    404     * @param mcDir the minecraft home directory
    405     * @return the lib directory
    406     */
    407    private static File setupLibDir(File mcDir)
    408    {
    409        File libDir = new File(mcDir,"lib");
    410        try
    411        {
    412            libDir = libDir.getCanonicalFile();
    413        }
    414        catch (IOException e)
    415        {
    416            throw new RuntimeException(String.format("Unable to canonicalize the lib dir at %s", mcDir.getName()),e);
    417        }
    418        if (!libDir.exists())
    419        {
    420            libDir.mkdir();
    421        }
    422        else if (libDir.exists() && !libDir.isDirectory())
    423        {
    424            throw new RuntimeException(String.format("Found a lib file in %s that's not a directory", mcDir.getName()));
    425        }
    426        return libDir;
    427    }
    428
    429    /**
    430     * @param mcDir the minecraft home directory
    431     * @return the coremod directory
    432     */
    433    private static File setupCoreModDir(File mcDir)
    434    {
    435        File coreModDir = new File(mcDir,"coremods");
    436        try
    437        {
    438            coreModDir = coreModDir.getCanonicalFile();
    439        }
    440        catch (IOException e)
    441        {
    442            throw new RuntimeException(String.format("Unable to canonicalize the coremod dir at %s", mcDir.getName()),e);
    443        }
    444        if (!coreModDir.exists())
    445        {
    446            coreModDir.mkdir();
    447        }
    448        else if (coreModDir.exists() && !coreModDir.isDirectory())
    449        {
    450            throw new RuntimeException(String.format("Found a coremod file in %s that's not a directory", mcDir.getName()));
    451        }
    452        return coreModDir;
    453    }
    454
    455    private static void downloadFile(File libFile, String rootUrl,String realFilePath, String hash)
    456    {
    457        try
    458        {
    459            URL libDownload = new URL(String.format(rootUrl,realFilePath));
    460            downloadMonitor.updateProgressString("Downloading file %s", libDownload.toString());
    461            FMLRelaunchLog.info("Downloading file %s", libDownload.toString());
    462            URLConnection connection = libDownload.openConnection();
    463            connection.setConnectTimeout(5000);
    464            connection.setReadTimeout(5000);
    465            connection.setRequestProperty("User-Agent", "FML Relaunch Downloader");
    466            int sizeGuess = connection.getContentLength();
    467            performDownload(connection.getInputStream(), sizeGuess, hash, libFile);
    468            downloadMonitor.updateProgressString("Download complete");
    469            FMLRelaunchLog.info("Download complete");
    470        }
    471        catch (Exception e)
    472        {
    473            if (downloadMonitor.shouldStopIt())
    474            {
    475                FMLRelaunchLog.warning("You have stopped the downloading operation before it could complete");
    476                return;
    477            }
    478            if (e instanceof RuntimeException) throw (RuntimeException)e;
    479            FMLRelaunchLog.severe("There was a problem downloading the file %s automatically. Perhaps you " +
    480                    "have an environment without internet access. You will need to download " +
    481                    "the file manually or restart and let it try again\n", libFile.getName());
    482            libFile.delete();
    483            throw new RuntimeException("A download error occured", e);
    484        }
    485    }
    486
    487    public static List<String> getLibraries()
    488    {
    489        return loadedLibraries;
    490    }
    491
    492    private static ByteBuffer downloadBuffer = ByteBuffer.allocateDirect(1 << 22);
    493    static IDownloadDisplay downloadMonitor;
    494
    495    private static void performDownload(InputStream is, int sizeGuess, String validationHash, File target)
    496    {
    497        if (sizeGuess > downloadBuffer.capacity())
    498        {
    499            throw new RuntimeException(String.format("The file %s is too large to be downloaded by FML - the coremod is invalid", target.getName()));
    500        }
    501        downloadBuffer.clear();
    502
    503        int bytesRead, fullLength = 0;
    504
    505        downloadMonitor.resetProgress(sizeGuess);
    506        try
    507        {
    508            downloadMonitor.setPokeThread(Thread.currentThread());
    509            byte[] smallBuffer = new byte[1024];
    510            while ((bytesRead = is.read(smallBuffer)) >= 0) {
    511                downloadBuffer.put(smallBuffer, 0, bytesRead);
    512                fullLength += bytesRead;
    513                if (downloadMonitor.shouldStopIt())
    514                {
    515                    break;
    516                }
    517                downloadMonitor.updateProgress(fullLength);
    518            }
    519            is.close();
    520            downloadMonitor.setPokeThread(null);
    521            downloadBuffer.limit(fullLength);
    522            downloadBuffer.position(0);
    523        }
    524        catch (InterruptedIOException e)
    525        {
    526            // We were interrupted by the stop button. We're stopping now.. clear interruption flag.
    527            Thread.interrupted();
    528            return;
    529        }
    530        catch (IOException e)
    531        {
    532            throw new RuntimeException(e);
    533        }
    534
    535
    536        try
    537        {
    538            String cksum = generateChecksum(downloadBuffer);
    539            if (cksum.equals(validationHash))
    540            {
    541                downloadBuffer.position(0);
    542                FileOutputStream fos = new FileOutputStream(target);
    543                fos.getChannel().write(downloadBuffer);
    544                fos.close();
    545            }
    546            else
    547            {
    548                throw new RuntimeException(String.format("The downloaded file %s has an invalid checksum %s (expecting %s). The download did not succeed correctly and the file has been deleted. Please try launching again.", target.getName(), cksum, validationHash));
    549            }
    550        }
    551        catch (Exception e)
    552        {
    553            if (e instanceof RuntimeException) throw (RuntimeException)e;
    554            throw new RuntimeException(e);
    555        }
    556
    557
    558
    559    }
    560
    561    private static String generateChecksum(ByteBuffer buffer)
    562    {
    563        return CertificateHelper.getFingerprint(buffer);
    564    }
    565}