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