package com.entityreborn.socpuppet.extensions; import com.entityreborn.socpuppet.extensions.annotations.SocBotPlugin; import com.entityreborn.socpuppet.extensions.annotations.Trigger; 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 java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; 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; /** * * @author Layton */ public class ExtensionManager { private static ExtensionManager instance; private final Map<URL, ExtensionTracker> extensions = new HashMap<>(); private final List<File> locations = new ArrayList<>(); public static ExtensionManager Get() { if (instance == null) { instance = new ExtensionManager(); } return instance; } public void cache(File extCache, File cacheDir) { // 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; } // Using System.out here instead of the logger as the logger doesn't // immediately print to the console. System.out.println("[SocPuppet] Caching extensions..."); // 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) { Logger.getLogger(ExtensionManager.class.getName()).log(Level.WARNING, "[SocPuppet] 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(cacheDir); DynamicClassLoader dcl = new DynamicClassLoader(); ClassDiscovery cd = new ClassDiscovery(); cd.setClassDiscoveryCache(cache); cd.addDiscoveryLocation(ClassDiscovery.GetClassContainer(ExtensionManager.class)); //Look in the given locations for jars, add them to our class discovery. List<File> toProcess = new ArrayList<File>(); 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) { Logger.getLogger(ExtensionManager.class.getName()).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( SocBotPlugin.class, AbstractExtension.class)) { AnnotationMirror plug = extmirror.getAnnotation(SocBotPlugin.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(plugURL.toURI()); } catch (URISyntaxException 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); } // Skip files already processed. if (done.contains(f)) { Logger.getLogger(ExtensionManager.class.getName()).log(Level.WARNING, f.getAbsolutePath() + " contains more than one extension" + " descriptor. Bug someone about it!"); 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++); Logger.getLogger(ExtensionManager.class.getName()).log(Level.WARNING, f.getAbsolutePath() + " contains a duplicate internally" + " named extension (" + name + "). Bug someone" + " about it!"); } 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) { Logger.getLogger(ExtensionManager.class.getName()).log( Level.SEVERE, "Could not copy '" + f.getName() + "' to cache: " + ex.getMessage()); } } } System.out.println("[SocPuppet] Extension caching complete."); // 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(); } /** * 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 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) { Logger.getLogger(ExtensionManager.class.getName()).log( Level.SEVERE, null, ex); } } } } else if (location.getName().endsWith(".jar")) { try { // Add the trimmed absolute path. toProcess.add(location.getCanonicalFile()); } catch (IOException ex) { Logger.getLogger(ExtensionManager.class.getName()).log( Level.SEVERE, null, ex); } } return toProcess; } /** * 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. * @param extCache */ public void initialize(File extCache, ClassDiscovery cd) { // Look in the given locations for jars, add them to our class discovery, // then initialize everything DynamicClassLoader dcl = new DynamicClassLoader(); List<File> toProcess = new ArrayList<>(); toProcess.addAll(getFiles(extCache)); for (File file : toProcess) { if (!file.canRead()) { continue; } URL jar; try { jar = file.toURI().toURL(); } catch (MalformedURLException ex) { Logger.getLogger(ExtensionManager.class.getName()).log( Level.SEVERE, null, ex); continue; } //First, load it with our custom class loader dcl.addJar(jar); cd.addDiscoveryLocation(jar); } cd.setDefaultClassLoader(dcl); for (ClassMirror<AbstractExtension> extmirror : cd.getClassesWithAnnotationThatExtend( SocBotPlugin.class, AbstractExtension.class)) { Extension plugin; Class<? extends AbstractExtension> extcls = extmirror.loadClass(dcl, true); URL url = ClassDiscovery.GetClassContainer(extcls); try { plugin = extcls.newInstance(); } catch (InstantiationException | IllegalAccessException ex) { //Error, but skip this one, don't throw an exception ourselves, just log it. Logger.getLogger(ExtensionManager.class.getName()).log(Level.SEVERE, "Could not instantiate " + extcls.getName() + ": " + ex.getMessage()); continue; } if (!extensions.containsKey(url)) { extensions.put(url, new ExtensionTracker(url, cd, dcl)); } extensions.get(url).addExtension(plugin); } for (ClassMirror<AbstractTrigger> extmirror : cd.getClassesWithAnnotationThatExtend(Trigger.class, AbstractTrigger.class)) { AbstractTrigger trig; Class<AbstractTrigger> extcls = extmirror.loadClass(dcl, true); URL url = ClassDiscovery.GetClassContainer(extcls); try { trig = extcls.newInstance(); } catch (InstantiationException | IllegalAccessException ex) { //Error, but skip this one, don't throw an exception ourselves, just log it. Logger.getLogger(ExtensionManager.class.getName()).log(Level.SEVERE, "Could not instantiate " + extcls.getName() + ": " + ex.getMessage()); continue; } if (!extensions.containsKey(url)) { extensions.put(url, new ExtensionTracker(url, cd, dcl)); } extensions.get(url).addTrigger(trig); } for (ClassMirror<ConsoleCommand> extmirror : cd.getClassesWithAnnotationThatExtend(Trigger.class, ConsoleCommand.class)) { ConsoleCommand trig; Class<ConsoleCommand> extcls = extmirror.loadClass(dcl, true); URL url = ClassDiscovery.GetClassContainer(extcls); try { trig = extcls.newInstance(); } catch (InstantiationException | IllegalAccessException ex) { //Error, but skip this one, don't throw an exception ourselves, just log it. Logger.getLogger(ExtensionManager.class.getName()).log(Level.SEVERE, "Could not instantiate " + extcls.getName() + ": " + ex.getMessage()); continue; } if (!extensions.containsKey(url)) { extensions.put(url, new ExtensionTracker(url, cd, dcl)); } extensions.get(url).addConsoleCommand(trig); } } public Map<URL, ExtensionTracker> getTrackers() { return Collections.unmodifiableMap(extensions); } /** * Get an extension tracker by name. * * @param name * @return */ public ExtensionTracker getExtensionTracker(String name) { if (name == null || name.trim().isEmpty()) { return null; } for (ExtensionTracker trk : extensions.values()) { if (name.trim().equalsIgnoreCase(trk.identifier.trim())) { return trk; } } return null; } /** * Get an extension tracker by URL. * * @param url * @return */ public ExtensionTracker getExtensionTracker(URL url) { return extensions.get(url); } /** * Get an extension tracker via class. * * @param possible * @return */ public ExtensionTracker getExtensionTracker(Class possible) { URL url = ClassDiscovery.GetClassContainer(possible); return getExtensionTracker(url); } public void addDiscoveryLocation(File file) { try { locations.add(file.getCanonicalFile()); } catch (IOException ex) { Logger.getLogger(ExtensionManager.class.getName()).log(Level.SEVERE, null, ex); } } public void startup() { for (ExtensionTracker ext : extensions.values()) { try { ext.startup(); } catch (Throwable t) { Logger.getLogger(ExtensionManager.class.getName()).log( Level.SEVERE, "Error while starting " + ext.getIdentifier(), t); } } } public void shutdown() { for (ExtensionTracker ext : extensions.values()) { try { ext.shutdown(); } catch (Throwable t) { Logger.getLogger(ExtensionManager.class.getName()).log( Level.SEVERE, "Error while shutting down " + ext.getIdentifier(), t); } } } }