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