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 for (IFMLLoadingPlugin plugin : loadPlugins) 284 { 285 downloadMonitor.updateProgressString("Running coremod plugin %s", plugin.getClass().getSimpleName()); 286 data.put("coremodLocation", pluginLocations.get(plugin)); 287 plugin.injectData(data); 288 String setupClass = plugin.getSetupClass(); 289 if (setupClass != null) 290 { 291 try 292 { 293 IFMLCallHook call = (IFMLCallHook) Class.forName(setupClass, true, actualClassLoader).newInstance(); 294 Map<String,Object> callData = new HashMap<String, Object>(); 295 callData.put("mcLocation", mcDir); 296 callData.put("classLoader", actualClassLoader); 297 callData.put("coremodLocation", pluginLocations.get(plugin)); 298 callData.put("deobfuscationFileName", FMLInjectionData.debfuscationDataName()); 299 call.injectData(callData); 300 call.call(); 301 } 302 catch (Exception e) 303 { 304 throw new RuntimeException(e); 305 } 306 } 307 downloadMonitor.updateProgressString("Coremod plugin %s run successfully", plugin.getClass().getSimpleName()); 308 309 String modContainer = plugin.getModContainerClass(); 310 if (modContainer != null) 311 { 312 FMLInjectionData.containers.add(modContainer); 313 } 314 } 315 try 316 { 317 downloadMonitor.updateProgressString("Validating minecraft"); 318 Class<?> loaderClazz = Class.forName("cpw.mods.fml.common.Loader", true, actualClassLoader); 319 Method m = loaderClazz.getMethod("injectData", Object[].class); 320 m.invoke(null, (Object)FMLInjectionData.data()); 321 m = loaderClazz.getMethod("instance"); 322 m.invoke(null); 323 downloadMonitor.updateProgressString("Minecraft validated, launching..."); 324 downloadBuffer = null; 325 } 326 catch (Exception e) 327 { 328 // Load in the Loader, make sure he's ready to roll - this will initialize most of the rest of minecraft here 329 System.out.println("A CRITICAL PROBLEM OCCURED INITIALIZING MINECRAFT - LIKELY YOU HAVE AN INCORRECT VERSION FOR THIS FML"); 330 throw new RuntimeException(e); 331 } 332 } 333 334 private static void discoverCoreMods(File mcDir, RelaunchClassLoader classLoader, List<IFMLLoadingPlugin> loadPlugins, List<ILibrarySet> libraries) 335 { 336 downloadMonitor.updateProgressString("Discovering coremods"); 337 File coreMods = setupCoreModDir(mcDir); 338 FilenameFilter ff = new FilenameFilter() 339 { 340 @Override 341 public boolean accept(File dir, String name) 342 { 343 return name.endsWith(".jar"); 344 } 345 }; 346 File[] coreModList = coreMods.listFiles(ff); 347 Arrays.sort(coreModList); 348 349 for (File coreMod : coreModList) 350 { 351 downloadMonitor.updateProgressString("Found a candidate coremod %s", coreMod.getName()); 352 JarFile jar; 353 Attributes mfAttributes; 354 try 355 { 356 jar = new JarFile(coreMod); 357 if (jar.getManifest() == null) 358 { 359 FMLRelaunchLog.warning("Found an un-manifested jar file in the coremods folder : %s, it will be ignored.", coreMod.getName()); 360 continue; 361 } 362 mfAttributes = jar.getManifest().getMainAttributes(); 363 } 364 catch (IOException ioe) 365 { 366 FMLRelaunchLog.log(Level.SEVERE, ioe, "Unable to read the coremod jar file %s - ignoring", coreMod.getName()); 367 continue; 368 } 369 370 String fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin"); 371 if (fmlCorePlugin == null) 372 { 373 FMLRelaunchLog.severe("The coremod %s does not contain a valid jar manifest- it will be ignored", coreMod.getName()); 374 continue; 375 } 376 377// String className = fmlCorePlugin.replace('.', '/').concat(".class"); 378// JarEntry ent = jar.getJarEntry(className); 379// if (ent ==null) 380// { 381// 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); 382// continue; 383// } 384// try 385// { 386// Class<?> coreModClass = Class.forName(fmlCorePlugin, false, classLoader); 387// FMLLog.severe("The coremods %s specified a class %s that is already present in the classpath - it will be ignored", coreMod.getName(), fmlCorePlugin); 388// continue; 389// } 390// catch (ClassNotFoundException cnfe) 391// { 392// // didn't find it, good 393// } 394 try 395 { 396 classLoader.addURL(coreMod.toURI().toURL()); 397 } 398 catch (MalformedURLException e) 399 { 400 FMLRelaunchLog.log(Level.SEVERE, e, "Unable to convert file into a URL. weird"); 401 continue; 402 } 403 try 404 { 405 downloadMonitor.updateProgressString("Loading coremod %s", coreMod.getName()); 406 classLoader.addTransformerExclusion(fmlCorePlugin); 407 Class<?> coreModClass = Class.forName(fmlCorePlugin, true, classLoader); 408 TransformerExclusions trExclusions = coreModClass.getAnnotation(IFMLLoadingPlugin.TransformerExclusions.class); 409 if (trExclusions!=null) 410 { 411 for (String st : trExclusions.value()) 412 { 413 classLoader.addTransformerExclusion(st); 414 } 415 } 416 IFMLLoadingPlugin plugin = (IFMLLoadingPlugin) coreModClass.newInstance(); 417 loadPlugins.add(plugin); 418 pluginLocations .put(plugin, coreMod); 419 if (plugin.getLibraryRequestClass()!=null) 420 { 421 for (String libName : plugin.getLibraryRequestClass()) 422 { 423 libraries.add((ILibrarySet) Class.forName(libName, true, classLoader).newInstance()); 424 } 425 } 426 downloadMonitor.updateProgressString("Loaded coremod %s", coreMod.getName()); 427 } 428 catch (ClassNotFoundException cnfe) 429 { 430 FMLRelaunchLog.log(Level.SEVERE, cnfe, "Coremod %s: Unable to class load the plugin %s", coreMod.getName(), fmlCorePlugin); 431 } 432 catch (ClassCastException cce) 433 { 434 FMLRelaunchLog.log(Level.SEVERE, cce, "Coremod %s: The plugin %s is not an implementor of IFMLLoadingPlugin", coreMod.getName(), fmlCorePlugin); 435 } 436 catch (InstantiationException ie) 437 { 438 FMLRelaunchLog.log(Level.SEVERE, ie, "Coremod %s: The plugin class %s was not instantiable", coreMod.getName(), fmlCorePlugin); 439 } 440 catch (IllegalAccessException iae) 441 { 442 FMLRelaunchLog.log(Level.SEVERE, iae, "Coremod %s: The plugin class %s was not accessible", coreMod.getName(), fmlCorePlugin); 443 } 444 } 445 } 446 447 /** 448 * @param mcDir the minecraft home directory 449 * @return the lib directory 450 */ 451 private static File setupLibDir(File mcDir) 452 { 453 File libDir = new File(mcDir,"lib"); 454 try 455 { 456 libDir = libDir.getCanonicalFile(); 457 } 458 catch (IOException e) 459 { 460 throw new RuntimeException(String.format("Unable to canonicalize the lib dir at %s", mcDir.getName()),e); 461 } 462 if (!libDir.exists()) 463 { 464 libDir.mkdir(); 465 } 466 else if (libDir.exists() && !libDir.isDirectory()) 467 { 468 throw new RuntimeException(String.format("Found a lib file in %s that's not a directory", mcDir.getName())); 469 } 470 return libDir; 471 } 472 473 /** 474 * @param mcDir the minecraft home directory 475 * @return the coremod directory 476 */ 477 private static File setupCoreModDir(File mcDir) 478 { 479 File coreModDir = new File(mcDir,"coremods"); 480 try 481 { 482 coreModDir = coreModDir.getCanonicalFile(); 483 } 484 catch (IOException e) 485 { 486 throw new RuntimeException(String.format("Unable to canonicalize the coremod dir at %s", mcDir.getName()),e); 487 } 488 if (!coreModDir.exists()) 489 { 490 coreModDir.mkdir(); 491 } 492 else if (coreModDir.exists() && !coreModDir.isDirectory()) 493 { 494 throw new RuntimeException(String.format("Found a coremod file in %s that's not a directory", mcDir.getName())); 495 } 496 return coreModDir; 497 } 498 499 private static void downloadFile(File libFile, String rootUrl,String realFilePath, String hash) 500 { 501 try 502 { 503 URL libDownload = new URL(String.format(rootUrl,realFilePath)); 504 downloadMonitor.updateProgressString("Downloading file %s", libDownload.toString()); 505 FMLRelaunchLog.info("Downloading file %s", libDownload.toString()); 506 URLConnection connection = libDownload.openConnection(); 507 connection.setConnectTimeout(5000); 508 connection.setReadTimeout(5000); 509 connection.setRequestProperty("User-Agent", "FML Relaunch Downloader"); 510 int sizeGuess = connection.getContentLength(); 511 performDownload(connection.getInputStream(), sizeGuess, hash, libFile); 512 downloadMonitor.updateProgressString("Download complete"); 513 FMLRelaunchLog.info("Download complete"); 514 } 515 catch (Exception e) 516 { 517 if (downloadMonitor.shouldStopIt()) 518 { 519 FMLRelaunchLog.warning("You have stopped the downloading operation before it could complete"); 520 return; 521 } 522 if (e instanceof RuntimeException) throw (RuntimeException)e; 523 FMLRelaunchLog.severe("There was a problem downloading the file %s automatically. Perhaps you " + 524 "have an environment without internet access. You will need to download " + 525 "the file manually or restart and let it try again\n", libFile.getName()); 526 libFile.delete(); 527 throw new RuntimeException("A download error occured", e); 528 } 529 } 530 531 public static List<String> getLibraries() 532 { 533 return loadedLibraries; 534 } 535 536 private static ByteBuffer downloadBuffer = ByteBuffer.allocateDirect(1 << 23); 537 static IDownloadDisplay downloadMonitor; 538 539 private static void performDownload(InputStream is, int sizeGuess, String validationHash, File target) 540 { 541 if (sizeGuess > downloadBuffer.capacity()) 542 { 543 throw new RuntimeException(String.format("The file %s is too large to be downloaded by FML - the coremod is invalid", target.getName())); 544 } 545 downloadBuffer.clear(); 546 547 int bytesRead, fullLength = 0; 548 549 downloadMonitor.resetProgress(sizeGuess); 550 try 551 { 552 downloadMonitor.setPokeThread(Thread.currentThread()); 553 byte[] smallBuffer = new byte[1024]; 554 while ((bytesRead = is.read(smallBuffer)) >= 0) { 555 downloadBuffer.put(smallBuffer, 0, bytesRead); 556 fullLength += bytesRead; 557 if (downloadMonitor.shouldStopIt()) 558 { 559 break; 560 } 561 downloadMonitor.updateProgress(fullLength); 562 } 563 is.close(); 564 downloadMonitor.setPokeThread(null); 565 downloadBuffer.limit(fullLength); 566 downloadBuffer.position(0); 567 } 568 catch (InterruptedIOException e) 569 { 570 // We were interrupted by the stop button. We're stopping now.. clear interruption flag. 571 Thread.interrupted(); 572 return; 573 } 574 catch (IOException e) 575 { 576 throw new RuntimeException(e); 577 } 578 579 580 try 581 { 582 String cksum = generateChecksum(downloadBuffer); 583 if (cksum.equals(validationHash)) 584 { 585 downloadBuffer.position(0); 586 FileOutputStream fos = new FileOutputStream(target); 587 fos.getChannel().write(downloadBuffer); 588 fos.close(); 589 } 590 else 591 { 592 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)); 593 } 594 } 595 catch (Exception e) 596 { 597 if (e instanceof RuntimeException) throw (RuntimeException)e; 598 throw new RuntimeException(e); 599 } 600 601 602 603 } 604 605 private static String generateChecksum(ByteBuffer buffer) 606 { 607 return CertificateHelper.getFingerprint(buffer); 608 } 609}