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