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