001/* 002 * Trident - A Multithreaded Server Alternative 003 * Copyright 2014 The TridentSDK Team 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package net.tridentsdk.server.plugin; 019 020import com.google.common.collect.ForwardingList; 021import com.google.common.collect.ImmutableList; 022import com.google.common.collect.Maps; 023import net.tridentsdk.Trident; 024import net.tridentsdk.concurrent.HeldValueLatch; 025import net.tridentsdk.concurrent.SelectableThread; 026import net.tridentsdk.docs.InternalUseOnly; 027import net.tridentsdk.event.Listener; 028import net.tridentsdk.plugin.Plugin; 029import net.tridentsdk.plugin.PluginLoadException; 030import net.tridentsdk.plugin.Plugins; 031import net.tridentsdk.plugin.annotation.IgnoreRegistration; 032import net.tridentsdk.plugin.annotation.PluginDesc; 033import net.tridentsdk.plugin.cmd.Command; 034import net.tridentsdk.registry.Registered; 035import net.tridentsdk.server.concurrent.ConcurrentTaskExecutor; 036import net.tridentsdk.server.concurrent.TickSync; 037import net.tridentsdk.util.TridentLogger; 038 039import java.io.File; 040import java.io.IOException; 041import java.lang.reflect.Constructor; 042import java.lang.reflect.InvocationTargetException; 043import java.lang.reflect.Modifier; 044import java.util.Enumeration; 045import java.util.List; 046import java.util.Map; 047import java.util.jar.JarEntry; 048import java.util.jar.JarFile; 049 050/** 051 * Handles server plugins, loading and unloading, class management, and lifecycle management for plugins 052 * <p> 053 * <p>To access this handler, use this code: 054 * <pre><code> 055 * PluginHandler handler = Registered.plugins(); 056 * </code></pre></p> 057 * 058 * @author The TridentSDK Team 059 * @since 0.3-alpha-DP 060 */ 061public class PluginHandler extends ForwardingList<Plugin> implements Plugins { 062 private static final SelectableThread EXECUTOR = ConcurrentTaskExecutor.create(1, "Plugins").selectCore(); 063 final Map<String, Plugin> plugins = Maps.newConcurrentMap(); // This need not be concurrent... but TridentLogger >.< 064 065 /** 066 * Do not instantiate this without being Trident 067 * <p> 068 * <p>To access this handler, use this code: 069 * <pre><code> 070 * TridentPluginHandler handler = Handler.plugins(); 071 * </code></pre></p> 072 */ 073 public PluginHandler() { 074 if (!Trident.isTrident()) 075 throw new RuntimeException(new IllegalAccessException("Can only be instantiated by Trident")); 076 } 077 078 @Override 079 @InternalUseOnly 080 public Plugin load(final File pluginFile) { 081 HeldValueLatch<Plugin> latch = HeldValueLatch.create(); 082 083 TickSync.sync(new Runnable() { 084 @Override 085 public void run() { 086 JarFile jarFile = null; 087 try { 088 // load all classes 089 jarFile = new JarFile(pluginFile); 090 PluginClassLoader loader = new PluginClassLoader(pluginFile, getClass().getClassLoader()); 091 Class<? extends Plugin> pluginClass = null; 092 093 Enumeration<JarEntry> entries = jarFile.entries(); 094 while (entries.hasMoreElements()) { 095 JarEntry entry = entries.nextElement(); 096 097 if (entry.isDirectory() || !entry.getName().endsWith(".class")) { 098 continue; 099 } 100 101 String name = entry.getName().replace(".class", "").replace('/', '.'); 102 Class<?> loadedClass = loader.loadClass(name); 103 104 loader.putClass(loadedClass); 105 106 if (Plugin.class.isAssignableFrom(loadedClass)) { 107 if (pluginClass != null) 108 TridentLogger.get().error(new PluginLoadException("Plugin has more than one main class!")); 109 110 pluginClass = loadedClass.asSubclass(Plugin.class); 111 } 112 } 113 114 // start initiating the plugin class and registering commands and listeners 115 if (pluginClass == null) { 116 TridentLogger.get().error(new PluginLoadException("Plugin does not have a main class")); 117 loader.unloadClasses(); 118 loader = null; // help gc 119 return; 120 } 121 122 PluginDesc description = pluginClass.getAnnotation(PluginDesc.class); 123 124 if (description == null) { 125 TridentLogger.get().error(new PluginLoadException("PluginDesc annotation does not exist!")); 126 loader.unloadClasses(); 127 loader = null; // help gc 128 return; 129 } 130 131 if (plugins.containsKey(description.name())) { 132 TridentLogger.get().error(new PluginLoadException("Plugin with name " + description.name() + 133 " has been loaded")); 134 loader.unloadClasses(); 135 loader = null; // help gc 136 return; 137 } 138 139 TridentLogger.get().log("Loading " + description.name() + " version " + description.version()); 140 141 Plugin plugin = pluginClass.newInstance(); 142 plugin.init(pluginFile, description, loader); 143 plugins.put(description.name(), plugin); 144 plugin.load(); 145 latch.countDown(plugin); 146 TridentLogger.get().success("Loaded " + description.name() + " version " + description.version()); 147 } catch (IOException | IllegalAccessException | InstantiationException | ClassNotFoundException ex) { // UNLOAD PLUGIN 148 TridentLogger.get().error(new PluginLoadException(ex)); 149 } finally { 150 if (jarFile != null) 151 try { 152 jarFile.close(); 153 } catch (IOException e) { 154 TridentLogger.get().error(e); 155 } 156 } 157 } 158 }); 159 160 try { 161 return latch.await(); 162 } catch (InterruptedException e) { 163 e.printStackTrace(); 164 return null; 165 } 166 } 167 168 @Override 169 public void enable(Plugin plugin) { 170 TridentLogger.get().log("Enabling " + plugin.description().name() + " version " + plugin.description().version()); 171 for (Class<?> cls : plugin.classLoader.loadedClasses().values()) { 172 try { 173 register(plugin, cls, EXECUTOR); 174 } catch (InstantiationException e) { 175 e.printStackTrace(); 176 } 177 } 178 179 TickSync.sync(plugin::enable); 180 TridentLogger.get().success("Enabled " + plugin.description().name() + " version " + plugin.description().version()); 181 } 182 183 private void register(Plugin plugin, Class<?> cls, SelectableThread executor) throws InstantiationException { 184 if (Modifier.isAbstract(cls.getModifiers())) 185 return; 186 187 Object instance = null; 188 Constructor<?> c = null; 189 190 try { 191 if (!cls.isAnnotationPresent(IgnoreRegistration.class)) { 192 if (Listener.class.isAssignableFrom(cls)) { 193 c = cls.getConstructor(); 194 Registered.events().registerListener(plugin, (Listener) (instance = c.newInstance())); 195 } 196 197 if (Command.class.isAssignableFrom(cls)) { 198 if (c == null) 199 c = cls.getConstructor(); 200 Registered.commands().register(plugin, (Command) (instance == null ? c.newInstance() : instance)); 201 } 202 } 203 } catch (NoSuchMethodException e) { 204 TridentLogger.get().error( 205 new PluginLoadException("A no-arg constructor for class " + cls.getName() + " does not exist")); 206 } catch (IllegalAccessException e) { 207 TridentLogger.get().error( 208 new PluginLoadException("A no-arg constructor for class " + cls.getName() + " is not accessible")); 209 } catch (InvocationTargetException e) { 210 e.printStackTrace(); 211 } 212 } 213 214 @Override 215 public void disable(final Plugin plugin) { 216 TickSync.sync(() -> { 217 // Perform disabling first, we don't want to unload everything 218 // then disable it 219 // State checking could be performed which breaks the class loader 220 plugin.disable(); 221 222 plugins.remove(plugin.description().name()); 223 224 plugin.classLoader.unloadClasses(); 225 plugin.classLoader = null; 226 }); 227 } 228 229 @Override 230 protected List<Plugin> delegate() { 231 return ImmutableList.copyOf(plugins.values()); 232 } 233 234 @Override 235 public SelectableThread executor() { 236 return EXECUTOR; 237 } 238}