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}