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