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