package com.laytonsmith.core.extensions; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscoveryCache; import com.laytonsmith.PureUtilities.ClassLoading.ClassMirror.AnnotationMirror; import com.laytonsmith.PureUtilities.ClassLoading.ClassMirror.ClassMirror; import com.laytonsmith.PureUtilities.ClassLoading.DynamicClassLoader; import com.laytonsmith.PureUtilities.Common.OSUtils; import com.laytonsmith.PureUtilities.Common.StackTraceUtils; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.SimpleVersion; import com.laytonsmith.abstraction.Implementation; import com.laytonsmith.annotations.api; import com.laytonsmith.commandhelper.CommandHelperFileLocations; import com.laytonsmith.core.AliasCore; import com.laytonsmith.core.CHLog; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.Prefs; import com.laytonsmith.core.Static; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.events.Driver; import com.laytonsmith.core.events.Event; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.functions.Function; import com.laytonsmith.core.functions.FunctionBase; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; public class ExtensionManager { private static final Map<URL, ExtensionTracker> extensions = new HashMap<>(); private static final List<File> locations = new ArrayList<>(); /** * Allow an external source (such as a Bukkit plugin) register it's own * functions and events. * EXPERIMENTAL! Could have bad side-effects! The use of this function is * for really advanced users. There is no guarantee of the fitness of this * function for ANY use. You have been warned. * @param url * @param tracker */ public static void RegisterTracker(URL url, ExtensionTracker tracker) { if (extensions.containsKey(url) || extensions.containsValue(tracker)) { return; } extensions.put(url, tracker); } /** * Allow external sources to unregister their own trackers. * EXPERIMENTAL! Could have bad side-effects! The use of this function is * for really advanced users. There is no guarantee of the fitness of this * function for ANY use. You have been warned. * @param url * @return */ public static ExtensionTracker UnregisterTracker(URL url) { if (!url.equals(ClassDiscovery.GetClassContainer(ExtensionManager.class))) { ExtensionTracker trk = extensions.remove(url); trk.shutdownTracker(); return trk; } return null; } /** * EXPERIMENTAL! could have bad side-effects! * Allow external sources to unregister their own trackers. * @param tracker * @return */ public static ExtensionTracker UnregisterTracker(ExtensionTracker tracker) { return UnregisterTracker(tracker.container); } /** * Process the given location for any jars. If the location is a jar, add it * directly. If the location is a directory, look for jars in it. * * @param location file or directory * @return */ private static List<File> getFiles(File location) { List<File> toProcess = new ArrayList<>(); if (location.isDirectory()) { for (File f : location.listFiles()) { if (f.getName().endsWith(".jar")) { try { // Add the trimmed absolute path. toProcess.add(f.getCanonicalFile()); } catch (IOException ex) { Static.getLogger().log(Level.SEVERE, "Could not get exact" + " path for " + f.getAbsolutePath(), ex); } } } } else if (location.getName().endsWith(".jar")) { try { // Add the trimmed absolute path. toProcess.add(location.getCanonicalFile()); } catch (IOException ex) { Static.getLogger().log(Level.SEVERE, "Could not get exact path" + " for " + location.getAbsolutePath(), ex); } } return toProcess; } public static Map<URL, ExtensionTracker> getTrackers() { return Collections.unmodifiableMap(extensions); } public static void Cache(File extCache, Class... extraClasses) { // We will only cache on Windows, as Linux doesn't natively lock // files that are in use. Windows prevents any modification, making // it harder for server owners on Windows to update the jars. boolean onWindows = (OSUtils.GetOS() == OSUtils.OS.WINDOWS); if (!onWindows) { return; } // Create the directory if it doesn't exist. extCache.mkdirs(); // Try to delete any loose files in the cache dir, so that we // don't load stuff we aren't supposed to. This is in case the shutdown // cleanup wasn't successful on the last run. for (File f : extCache.listFiles()) { try { Files.delete(f.toPath()); } catch (IOException ex) { Static.getLogger().log(Level.WARNING, "[CommandHelper] Could not delete loose file " + f.getAbsolutePath() + ": " + ex.getMessage()); } } // The cache, cd and dcl here will just be thrown away. // They are only used here for the purposes of discovering what a given // jar has to offer. ClassDiscoveryCache cache = new ClassDiscoveryCache( CommandHelperFileLocations.getDefault().getCacheDirectory()); cache.setLogger(Static.getLogger()); DynamicClassLoader dcl = new DynamicClassLoader(); ClassDiscovery cd = new ClassDiscovery(); cd.setClassDiscoveryCache(cache); cd.addDiscoveryLocation(ClassDiscovery.GetClassContainer(ExtensionManager.class)); for (Class klazz: extraClasses) { cd.addDiscoveryLocation(ClassDiscovery.GetClassContainer(klazz)); } //Look in the given locations for jars, add them to our class discovery. List<File> toProcess = new ArrayList<>(); for (File location : locations) { toProcess.addAll(getFiles(location)); } // Load the files into the discovery mechanism. for (File file : toProcess) { if (!file.canRead()) { continue; } URL jar; try { jar = file.toURI().toURL(); } catch (MalformedURLException ex) { Static.getLogger().log(Level.SEVERE, null, ex); continue; } dcl.addJar(jar); cd.addDiscoveryLocation(jar); } cd.setDefaultClassLoader(dcl); // Loop thru the found lifecycles, copy them to the cache using the name // given in the lifecycle. If more than one jar has the same internal // name, the filename will be given a number. Set<File> done = new HashSet<>(); Map<String, Integer> namecount = new HashMap<>(); // First, cache new lifecycle style extensions. They will be renamed to // use their internal name. for (ClassMirror<AbstractExtension> extmirror : cd.getClassesWithAnnotationThatExtend( MSExtension.class, AbstractExtension.class)) { AnnotationMirror plug = extmirror.getAnnotation(MSExtension.class); URL plugURL = extmirror.getContainer(); // Get the internal name that this extension exposes. if (plugURL != null && plugURL.getPath().endsWith(".jar")) { File f; try { f = new File(URLDecoder.decode(plugURL.getFile(), "UTF8")); } catch (UnsupportedEncodingException ex) { Logger.getLogger(ExtensionManager.class.getName()).log(Level.SEVERE, null, ex); continue; } // Skip extensions that originate from commandhelpercore. if (plugURL.equals(ClassDiscovery.GetClassContainer(ExtensionManager.class))) { done.add(f); continue; } // Skip files already processed. if (done.contains(f)) { CHLog.GetLogger().Log(CHLog.Tags.EXTENSIONS, LogLevel.WARNING, f.getAbsolutePath() + " contains more than one extension" + " descriptor. Bug someone about it!", Target.UNKNOWN); continue; } done.add(f); String name = plug.getValue("value").toString(); // Just in case we have two plugins with the same internal name, // lets track and rename them using a number scheme. if (namecount.containsKey(name.toLowerCase())) { int i = namecount.get(name.toLowerCase()); name += "-" + i; namecount.put(name.toLowerCase(), i++); CHLog.GetLogger().Log(CHLog.Tags.EXTENSIONS, LogLevel.WARNING, f.getAbsolutePath() + " contains a duplicate internally" + " named extension (" + name + "). Bug someone" + " about it!", Target.UNKNOWN); } else { namecount.put(name.toLowerCase(), 1); } // Rename the jar to use the plugin's internal name and // copy it into the cache. File newFile = new File(extCache, name.toLowerCase() + ".jar"); try { Files.copy(f.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (IOException ex) { Static.getLogger().log(Level.SEVERE, "Could not copy '" + f.getName() + "' to cache: " + ex.getMessage()); } } } Set<ClassMirror<?>> classes = cd.getClassesWithAnnotation(api.class); // Now process @api annotated extensions, ignoring ones already processed. for (ClassMirror klass : classes) { URL plugURL = klass.getContainer(); if (plugURL != null && plugURL.getPath().endsWith(".jar")) { File f; try { f = new File(URLDecoder.decode(plugURL.getFile(), "UTF8")); } catch (UnsupportedEncodingException ex) { Logger.getLogger(ExtensionManager.class.getName()).log(Level.SEVERE, null, ex); continue; } // Skip files already processed. if (done.contains(f)) { continue; } // Copy the file if it's a valid extension. // No special processing needed. if (cd.doesClassExtend(klass, Event.class) || cd.doesClassExtend(klass, Function.class)) { // We're processing it here instead of above, complain about it. CHLog.GetLogger().Log(CHLog.Tags.EXTENSIONS, LogLevel.WARNING, f.getAbsolutePath() + " is an old-style extension!" + " Bug the author to update it to the new extension system!", Target.UNKNOWN); // Only process this file once. done.add(f); File newFile = new File(extCache, "oldstyle-" + f.getName()); try { Files.copy(f.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (IOException ex) { Static.getLogger().log(Level.SEVERE, "Could not copy '" + f.getName() + "' to cache: " + ex.getMessage()); } } } } // Shut down the original dcl to "unlock" the processed jars. // The cache and cd instances will just fall into oblivion. dcl.destroy(); // Explicit call. Without this, jar files won't actually get unlocked on // Windows. Of course, this is hit and miss, but that's fine; we tried. System.gc(); } /** * Initializes the extension manager. This operation is not necessarily * required, and must be guaranteed to not run more than once per * ClassDiscovery object. * * @param cd the ClassDiscovery to use for loading files. */ public static void Initialize(ClassDiscovery cd) { extensions.clear(); // Look in the extension folder for jars, add them to our class discover, // then initialize everything List<File> toProcess = new ArrayList<>(); // Grab files from the cache if on Windows. Otherwise just load // directly from the stored locations. boolean onWindows = (OSUtils.GetOS() == OSUtils.OS.WINDOWS); if (onWindows) { toProcess.addAll(getFiles(CommandHelperFileLocations.getDefault().getExtensionCacheDirectory())); } else { for (File location : locations) { toProcess.addAll(getFiles(location)); } } DynamicClassLoader dcl = new DynamicClassLoader(); cd.setDefaultClassLoader(dcl); for (File f : toProcess) { if (f.getName().endsWith(".jar")) { try { //First, load it with our custom class loader URL jar = f.toURI().toURL(); dcl.addJar(jar); cd.addDiscoveryLocation(jar); CHLog.GetLogger().Log(CHLog.Tags.EXTENSIONS, LogLevel.DEBUG, "Loaded " + f.getAbsolutePath(), Target.UNKNOWN); } catch (MalformedURLException ex) { Static.getLogger().log(Level.SEVERE, null, ex); } } } // Grab all known lifecycle classes, and use them. If more than one // lifecycle is found per URL, it's stored and used, but the first // one found defines the internal name. for (ClassMirror<AbstractExtension> extmirror : cd.getClassesWithAnnotationThatExtend(MSExtension.class, AbstractExtension.class)) { Extension ext; URL url = extmirror.getContainer(); Class<AbstractExtension> extcls; if (extmirror.getModifiers().isAbstract()) { Static.getLogger().log(Level.SEVERE, "Probably won't be able to" + " instantiate " + extmirror.getClassName() + ": The" + " class is marked as abstract! Will try anyway."); } try { extcls = extmirror.loadClass(dcl, true); } catch (Throwable ex) { // May throw anything, and kill the loading process. // Lets prevent that! Static.getLogger().log(Level.SEVERE, "Could not load class '" + extmirror.getClassName() + "'"); ex.printStackTrace(); continue; } try { ext = extcls.newInstance(); } catch (InstantiationException | IllegalAccessException ex) { //Error, but skip this one, don't throw an exception ourselves, just log it. Static.getLogger().log(Level.SEVERE, "Could not instantiate " + extcls.getName() + ": " + ex.getMessage()); continue; } ExtensionTracker trk = extensions.get(url); if (trk == null) { trk = new ExtensionTracker(url, cd, dcl); extensions.put(url, trk); } // Grab the identifier for the first lifecycle we come across and // use it. if (trk.identifier == null) { trk.identifier = ext.getName(); try { trk.version = ext.getVersion(); } catch(AbstractMethodError ex){ // getVersion() was added later. This is a temporary fix // to allow extension authors some time to update. // TODO: Remove this soon. trk.version = new SimpleVersion("0.0.0"); } } trk.allExtensions.add(ext); } // Lets store info about the functions and events extensions have. // This will aide in gracefully unloading stuff later. Set<ClassMirror<?>> classes = cd.getClassesWithAnnotation(api.class); // Temp tracking for loading messages later on. List<String> events = new ArrayList<>(); List<String> functions = new ArrayList<>(); // Loop over the classes, instantiate and register functions and events, // and store the instances in their trackers. for (ClassMirror klass : classes) { URL url = klass.getContainer(); if (cd.doesClassExtend(klass, Event.class) || cd.doesClassExtend(klass, Function.class)) { Class c; try { c = klass.loadClass(dcl, true); } catch (Throwable ex) { // May throw anything, and kill the loading process. // Lets prevent that! Static.getLogger().log(Level.SEVERE, "Could not load class '" + klass.getClassName() + "'"); ex.printStackTrace(); continue; } ExtensionTracker trk = extensions.get(url); if (trk == null) { trk = new ExtensionTracker(url, cd, dcl); extensions.put(url, trk); } // Instantiate, register and store. try { if (Event.class.isAssignableFrom(c)) { Class<Event> cls = (Class<Event>) c; if (klass.getModifiers().isAbstract()) { // Abstract? Looks like they accidently @api'd // a cheater class. We can't be sure that it is fully // defined, so complain to the console. CHLog.GetLogger().Log(CHLog.Tags.EXTENSIONS, LogLevel.ERROR, "Class " + c.getName() + " in " + url + " is" + " marked as an event but is also abstract." + " Bugs might occur! Bug someone about this!", Target.UNKNOWN); } Event e = cls.newInstance(); events.add(e.getName()); trk.registerEvent(e); } else if (Function.class.isAssignableFrom(c)) { Class<Function> cls = (Class<Function>) c; if (klass.getModifiers().isAbstract()) { // Abstract? Looks like they accidently @api'd // a cheater class. We can't be sure that it is fully // defined, so complain to the console. CHLog.GetLogger().Log(CHLog.Tags.EXTENSIONS, LogLevel.ERROR, "Class " + c.getName() + " in " + url + " is" + " marked as a function but is also abstract." + " Bugs might occur! Bug someone about this!", Target.UNKNOWN); } Function f = cls.newInstance(); functions.add(f.getName()); trk.registerFunction(f); } } catch (InstantiationException ex) { Static.getLogger().log(Level.SEVERE, ex.getMessage(), ex); } catch (IllegalAccessException ex) { Static.getLogger().log(Level.SEVERE, null, ex); } } } // Lets print out the details to the console, if we are in debug mode. try{ if (Prefs.DebugMode()) { Collections.sort(events); String eventString = StringUtils.Join(events, ", ", ", and ", " and "); Collections.sort(functions); String functionString = StringUtils.Join(functions, ", ", ", and ", " and "); StreamUtils.GetSystemOut().println(Implementation.GetServerType().getBranding() + ": Loaded the following functions: " + functionString.trim()); StreamUtils.GetSystemOut().println(Implementation.GetServerType().getBranding() + ": Loaded " + functions.size() + " function" + (functions.size() == 1 ? "." : "s.")); StreamUtils.GetSystemOut().println(Implementation.GetServerType().getBranding() + ": Loaded the following events: " + eventString.trim()); StreamUtils.GetSystemOut().println(Implementation.GetServerType().getBranding() + ": Loaded " + events.size() + " event" + (events.size() == 1 ? "." : "s.")); } } catch(Throwable e) { // Prefs weren't loaded, probably caused by running tests. } } /** * To be run when we are shutting everything down. */ public static void Cleanup() { // Shutdown and release all the extensions Shutdown(); for (ExtensionTracker trk : extensions.values()) { trk.shutdownTracker(); } extensions.clear(); // Clean up the loaders and discovery instances. ClassDiscovery.getDefaultInstance().invalidateCaches(); ClassLoader loader = ClassDiscovery.getDefaultInstance().getDefaultClassLoader(); if (loader instanceof DynamicClassLoader) { DynamicClassLoader dcl = (DynamicClassLoader) loader; dcl.destroy(); } // Explicit call. Without this, jar files won't actually get unlocked on // Windows. Of course, this is hit and miss, but that's fine; we tried. System.gc(); File cacheDir = CommandHelperFileLocations.getDefault().getExtensionCacheDirectory(); if (!cacheDir.exists() || !cacheDir.isDirectory()) { return; } // Try to delete any loose files in the cache dir. for (File f : cacheDir.listFiles()) { try { Files.delete(f.toPath()); } catch (IOException ex) { StreamUtils.GetSystemOut().println("[CommandHelper] Could not delete loose file " + f.getAbsolutePath() + ": " + ex.getMessage()); } } } /** * This should be run each time the "startup" of the runtime occurs or extensions are reloaded. */ public static void Startup(){ for(ExtensionTracker trk : extensions.values()) { for(Extension ext : trk.getExtensions()) { try { ext.onStartup(); } catch(Throwable e){ Logger log = Static.getLogger(); log.log(Level.SEVERE, ext.getClass().getName() + "'s onStartup caused an exception:"); log.log(Level.SEVERE, StackTraceUtils.GetStacktrace(e)); } } } } /** * This should be run each time the "shutdown" of the runtime occurs or extensions are reloaded. */ public static void Shutdown(){ for(ExtensionTracker trk : extensions.values()) { for(Extension ext : trk.getExtensions()) { try { ext.onShutdown(); } catch(Throwable e){ Logger log = Static.getLogger(); log.log(Level.SEVERE, ext.getClass().getName() + "'s onShutdown caused an exception:"); log.log(Level.SEVERE, StackTraceUtils.GetStacktrace(e)); } } } } public static void PreReloadAliases(AliasCore.ReloadOptions options) { for (ExtensionTracker trk : extensions.values()) { for (Extension ext: trk.getExtensions()) { try { ext.onPreReloadAliases(options); } catch (Throwable e) { Logger log = Static.getLogger(); log.log(Level.SEVERE, ext.getClass().getName() + "'s onPreReloadAliases caused an exception:"); log.log(Level.SEVERE, StackTraceUtils.GetStacktrace(e)); } } } } public static void PostReloadAliases() { for (ExtensionTracker trk : extensions.values()) { for (Extension ext: trk.getExtensions()) { try { ext.onPostReloadAliases(); } catch (Throwable e) { Logger log = Static.getLogger(); log.log(Level.SEVERE, ext.getClass().getName() + "'s onPreReloadAliases caused an exception:"); log.log(Level.SEVERE, StackTraceUtils.GetStacktrace(e)); } } } } public static void AddDiscoveryLocation(File file) { try { locations.add(file.getCanonicalFile()); } catch (IOException ex) { Static.getLogger().log(Level.SEVERE, null, ex); } } public static Set<Event> GetEvents() { Set<Event> retn = new HashSet<>(); for (ExtensionTracker trk: extensions.values()) { retn.addAll(trk.getEvents()); } return retn; } public static Set<Event> GetEvents(Driver type) { Set<Event> retn = new HashSet<>(); for (ExtensionTracker trk: extensions.values()) { retn.addAll(trk.getEvents(type)); } return retn; } public static Event GetEvent(Driver type, String name) { for (ExtensionTracker trk: extensions.values()) { Set<Event> events = trk.getEvents(type); for (Event event: events) { if (event.getName().equalsIgnoreCase(name)) { return event; } } } return null; } public static Event GetEvent(String name) { for (ExtensionTracker trk: extensions.values()) { Set<Event> events = trk.getEvents(); for (Event event: events) { if (event.getName().equalsIgnoreCase(name)) { return event; } } } return null; } /** * This runs the hooks on all events. This should be called each time * the server "starts up". */ public static void RunHooks(){ for(Event event : GetEvents()){ try{ event.hook(); } catch(UnsupportedOperationException ex){} } } public static FunctionBase GetFunction(Construct c, api.Platforms platform) throws ConfigCompileException { if(platform == null){ //Default to the Java interpreter platform = api.Platforms.INTERPRETER_JAVA; } if (c instanceof CFunction) { for (ExtensionTracker trk: extensions.values()) { if(trk.functions.get(platform).containsKey(c.val()) && trk.supportedPlatforms.get(c.val()).contains(platform)){ return trk.functions.get(platform).get(c.val()); } } throw new ConfigCompileException("The function \"" + c.val() + "\" does not exist in the " + platform.platformName(), c.getTarget()); } else { throw new ConfigCompileException("Expecting CFunction type", c.getTarget()); } } public static Set<FunctionBase> GetFunctions(api.Platforms platform) { if(platform == null){ Set<FunctionBase> retn = new HashSet<>(); for(api.Platforms p : api.Platforms.values()){ retn.addAll(GetFunctions(p)); } return retn; } Set<FunctionBase> retn = new HashSet<>(); for (ExtensionTracker trk: extensions.values()) { for(FunctionBase func : trk.functions.get(platform).values()){ retn.add(func); } } return retn; } }