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}