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