All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.nukkit.plugin.PluginManager Maven / Gradle / Ivy

There is a newer version: 1.20.40-r1
Show newest version
package cn.nukkit.plugin;

import cn.nukkit.Server;
import cn.nukkit.api.*;
import cn.nukkit.command.PluginCommand;
import cn.nukkit.command.SimpleCommandMap;
import cn.nukkit.event.*;
import cn.nukkit.lang.BaseLang;
import cn.nukkit.permission.Permissible;
import cn.nukkit.permission.Permission;
import cn.nukkit.plugin.js.JSFeatures;
import cn.nukkit.utils.PluginException;
import cn.nukkit.utils.Utils;
import co.aikar.timings.Timing;
import co.aikar.timings.Timings;
import io.netty.util.internal.EmptyArrays;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;

/**
 * @author MagicDroidX
 */
@Log4j2
public class PluginManager {

    private final Server server;

    private final SimpleCommandMap commandMap;

    protected final Map plugins = new LinkedHashMap<>();

    protected final Map permissions = new HashMap<>();

    protected final Map defaultPerms = new HashMap<>();

    protected final Map defaultPermsOp = new HashMap<>();

    protected final Map> permSubs = new HashMap<>();

    protected final Set defSubs = Collections.newSetFromMap(new WeakHashMap<>());

    protected final Set defSubsOp = Collections.newSetFromMap(new WeakHashMap<>());

    @PowerNukkitXOnly
    @Getter
    protected final Map fileAssociations = new HashMap<>();

    public PluginManager(Server server, SimpleCommandMap commandMap) {
        this.server = server;
        this.commandMap = commandMap;
    }

    public Plugin getPlugin(String name) {
        if (this.plugins.containsKey(name)) {
            return this.plugins.get(name);
        }
        return null;
    }

    public boolean registerInterface(Class loaderClass) {
        if (loaderClass != null) {
            try {
                Constructor constructor = loaderClass.getDeclaredConstructor(Server.class);
                constructor.setAccessible(true);
                this.fileAssociations.put(loaderClass.getName(), (PluginLoader) constructor.newInstance(this.server));
                return true;
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }

    @PowerNukkitOnly
    @Since("1.3.0.0-PN")
    @PowerNukkitXDifference(info = "rename")
    public void loadInternalPlugin() {
        PluginLoader pluginLoader = fileAssociations.get(JavaPluginLoader.class.getName());
        InternalPlugin plugin = InternalPlugin.INSTANCE;
        Map info = new HashMap<>();
        info.put("name", "PowerNukkitX");
        info.put("version", server.getNukkitVersion());
        info.put("website", "https://github.com/PowerNukkitX/PowerNukkitX");
        info.put("main", InternalPlugin.class.getName());
        File file;
        try {
            file = new File(Server.class.getProtectionDomain().getCodeSource().getLocation().toURI());
        } catch (Exception e) {
            file = new File(".");
        }
        PluginDescription description = new PluginDescription(info);
        plugin.init(pluginLoader, server, description, new File("PowerNukkitX"), file);
        plugins.put(description.getName(), plugin);
        enablePlugin(plugin);
    }

    public Map getPlugins() {
        return plugins;
    }

    public Plugin loadPlugin(String path) {
        return this.loadPlugin(path, null);
    }

    public Plugin loadPlugin(File file) {
        return this.loadPlugin(file, null);
    }

    public Plugin loadPlugin(String path, Map loaders) {
        return this.loadPlugin(new File(path), loaders);
    }

    public Plugin loadPlugin(File file, Map loaders) {
        for (PluginLoader loader : (loaders == null ? this.fileAssociations : loaders).values()) {
            for (Pattern pattern : loader.getPluginFilters()) {
                if (pattern.matcher(file.getName()).matches()) {
                    PluginDescription description = loader.getPluginDescription(file);
                    if (description != null) {
                        try {
                            Plugin plugin = loader.loadPlugin(file);
                            if (plugin != null) {
                                this.plugins.put(plugin.getDescription().getName(), plugin);

                                List pluginCommands = this.parseYamlCommands(plugin);

                                if (!pluginCommands.isEmpty()) {
                                    this.commandMap.registerAll(plugin.getDescription().getName(), pluginCommands);
                                }

                                return plugin;
                            }
                        } catch (Exception e) {
                            log.fatal("Could not load plugin", e);
                            return null;
                        }
                    }
                }
            }
        }

        return null;
    }

    public Map loadPlugins(String dictionary) {
        return this.loadPlugins(new File(dictionary));
    }

    public Map loadPlugins(File dictionary) {
        return this.loadPlugins(dictionary, null);
    }

    public Map loadPlugins(String dictionary, List newLoaders) {
        return this.loadPlugins(new File(dictionary), newLoaders);
    }

    public Map loadPlugins(File dictionary, List newLoaders) {
        return this.loadPlugins(dictionary, newLoaders, false);
    }

    public Map loadPlugins(File dictionary, List newLoaders, boolean includeDir) {
        if (dictionary.isDirectory()) {
            Map plugins = new LinkedHashMap<>();
            Map loadedPlugins = new LinkedHashMap<>();
            Map> dependencies = new LinkedHashMap<>();
            Map> softDependencies = new LinkedHashMap<>();
            Map loaders = new LinkedHashMap<>();
            if (newLoaders != null) {
                for (String key : newLoaders) {
                    if (this.fileAssociations.containsKey(key)) {
                        loaders.put(key, this.fileAssociations.get(key));
                    }
                }
            } else {
                loaders = this.fileAssociations;
            }

            JSFeatures.initInternalFeatures();

            for (final PluginLoader loader : loaders.values()) {
                for (File file : dictionary.listFiles((dir, name) -> {
                    for (Pattern pattern : loader.getPluginFilters()) {
                        if (pattern.matcher(name).matches()) {
                            return true;
                        }
                    }
                    return false;
                })) {
                    if ((file.isDirectory() && !file.getName().startsWith("@")) && !includeDir) {
                        continue;
                    }
                    try {
                        PluginDescription description = loader.getPluginDescription(file);
                        if (description != null) {
                            String name = description.getName();

                            if (plugins.containsKey(name) || this.getPlugin(name) != null) {
                                log.error(this.server.getLanguage().tr("nukkit.plugin.duplicateError", name));
                                continue;
                            }

                            boolean compatible = false;

                            for (String version : description.getCompatibleAPIs()) {

                                try {
                                    //Check the format: majorVersion.minorVersion.patch
                                    if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", version)) {
                                        throw new IllegalArgumentException("The getCompatibleAPI version don't match the format majorVersion.minorVersion.patch");
                                    }
                                } catch (NullPointerException | IllegalArgumentException e) {
                                    log.error(this.server.getLanguage().tr("nukkit.plugin.loadError", new String[]{name, "Wrong API format"}), e);
                                    continue;
                                }

                                String[] versionArray = version.split("\\.");
                                String[] apiVersion = this.server.getApiVersion().split("\\.");

                                //Completely different API version
                                if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
                                    continue;
                                }

                                //If the plugin requires new API features, being backwards compatible
                                if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
                                    continue;
                                }

                                compatible = true;
                                break;
                            }

                            if (!compatible) {
                                log.error(this.server.getLanguage().tr("nukkit.plugin.loadError", new String[]{name, "%nukkit.plugin.incompatibleAPI"}));
                            }

                            plugins.put(name, file);

                            softDependencies.put(name, description.getSoftDepend());

                            dependencies.put(name, description.getDepend());

                            for (String before : description.getLoadBefore()) {
                                if (softDependencies.containsKey(before)) {
                                    softDependencies.get(before).add(name);
                                } else {
                                    List list = new ArrayList<>();
                                    list.add(name);
                                    softDependencies.put(before, list);
                                }
                            }
                        }
                    } catch (Exception e) {
                        log.error(this.server.getLanguage().tr("nukkit.plugin" +
                                ".fileError", file.getName(), dictionary.toString(), Utils
                                .getExceptionMessage(e)), e);
                    }
                }
            }

            while (!plugins.isEmpty()) {
                boolean missingDependency = true;
                for (String name : new ArrayList<>(plugins.keySet())) {
                    File file = plugins.get(name);
                    if (dependencies.containsKey(name)) {
                        for (String dependency : new ArrayList<>(dependencies.get(name))) {
                            if (loadedPlugins.containsKey(dependency) || this.getPlugin(dependency) != null) {
                                dependencies.get(name).remove(dependency);
                            } else if (!plugins.containsKey(dependency)) {
                                BaseLang language = this.server.getLanguage();
                                String cause = language.tr("nukkit.plugin.missingDependency", dependency);
                                log.fatal(language.tr("nukkit.plugin.loadError", name, cause));
                                break;
                            }
                        }

                        if (dependencies.get(name).isEmpty()) {
                            dependencies.remove(name);
                        }
                    }

                    if (softDependencies.containsKey(name)) {
                        softDependencies.get(name).removeIf(dependency ->
                                loadedPlugins.containsKey(dependency) || this.getPlugin(dependency) != null);

                        if (softDependencies.get(name).isEmpty()) {
                            softDependencies.remove(name);
                        }
                    }

                    if (!dependencies.containsKey(name) && !softDependencies.containsKey(name)) {
                        plugins.remove(name);
                        missingDependency = false;
                        Plugin plugin = this.loadPlugin(file, loaders);
                        if (plugin != null) {
                            loadedPlugins.put(name, plugin);
                        } else {
                            log.fatal(this.server.getLanguage().tr("nukkit.plugin.genericLoadError", name));
                        }
                    }
                }

                if (missingDependency) {
                    for (String name : new ArrayList<>(plugins.keySet())) {
                        File file = plugins.get(name);
                        if (!dependencies.containsKey(name)) {
                            softDependencies.remove(name);
                            plugins.remove(name);
                            missingDependency = false;
                            Plugin plugin = this.loadPlugin(file, loaders);
                            if (plugin != null) {
                                loadedPlugins.put(name, plugin);
                            } else {
                                log.fatal(this.server.getLanguage().tr("nukkit.plugin.genericLoadError", name));
                            }
                        }
                    }

                    if (missingDependency) {
                        for (String name : plugins.keySet()) {
                            log.fatal(this.server.getLanguage().tr("nukkit.plugin.loadError", new String[]{name, "%nukkit.plugin.circularDependency"}));
                        }
                        plugins.clear();
                    }
                }
            }

            return loadedPlugins;
        } else {
            return new HashMap<>();
        }
    }

    public Permission getPermission(String name) {
        if (this.permissions.containsKey(name)) {
            return this.permissions.get(name);
        }
        return null;
    }

    public boolean addPermission(Permission permission) {
        if (!this.permissions.containsKey(permission.getName())) {
            this.permissions.put(permission.getName(), permission);
            this.calculatePermissionDefault(permission);

            return true;
        }

        return false;
    }

    public void removePermission(String name) {
        this.permissions.remove(name);
    }

    public void removePermission(Permission permission) {
        this.removePermission(permission.getName());
    }

    public Map getDefaultPermissions(boolean op) {
        if (op) {
            return this.defaultPermsOp;
        } else {
            return this.defaultPerms;
        }
    }

    public void recalculatePermissionDefaults(Permission permission) {
        if (this.permissions.containsKey(permission.getName())) {
            this.defaultPermsOp.remove(permission.getName());
            this.defaultPerms.remove(permission.getName());
            this.calculatePermissionDefault(permission);
        }
    }

    private void calculatePermissionDefault(Permission permission) {
        Timings.permissionDefaultTimer.startTiming();
        if (permission.getDefault().equals(Permission.DEFAULT_OP) || permission.getDefault().equals(Permission.DEFAULT_TRUE)) {
            this.defaultPermsOp.put(permission.getName(), permission);
            this.dirtyPermissibles(true);
        }

        if (permission.getDefault().equals(Permission.DEFAULT_NOT_OP) || permission.getDefault().equals(Permission.DEFAULT_TRUE)) {
            this.defaultPerms.put(permission.getName(), permission);
            this.dirtyPermissibles(false);
        }
        Timings.permissionDefaultTimer.startTiming();
    }

    private void dirtyPermissibles(boolean op) {
        for (Permissible p : this.getDefaultPermSubscriptions(op)) {
            p.recalculatePermissions();
        }
    }

    public void subscribeToPermission(String permission, Permissible permissible) {
        if (!this.permSubs.containsKey(permission)) {
            this.permSubs.put(permission, Collections.newSetFromMap(new WeakHashMap<>()));
        }
        this.permSubs.get(permission).add(permissible);
    }

    public void unsubscribeFromPermission(String permission, Permissible permissible) {
        if (this.permSubs.containsKey(permission)) {
            this.permSubs.get(permission).remove(permissible);
            if (this.permSubs.get(permission).size() == 0) {
                this.permSubs.remove(permission);
            }
        }
    }

    public Set getPermissionSubscriptions(String permission) {
        if (this.permSubs.containsKey(permission)) {
            return new HashSet<>(this.permSubs.get(permission));
        }
        return new HashSet<>();
    }

    public void subscribeToDefaultPerms(boolean op, Permissible permissible) {
        if (op) {
            this.defSubsOp.add(permissible);
        } else {
            this.defSubs.add(permissible);
        }
    }

    public void unsubscribeFromDefaultPerms(boolean op, Permissible permissible) {
        if (op) {
            this.defSubsOp.remove(permissible);
        } else {
            this.defSubs.remove(permissible);
        }
    }

    public Set getDefaultPermSubscriptions(boolean op) {
        if (op) {
            return new HashSet<>(this.defSubsOp);
        } else {
            return new HashSet<>(this.defSubs);
        }
    }

    public Map getPermissions() {
        return permissions;
    }

    public boolean isPluginEnabled(Plugin plugin) {
        if (plugin != null && this.plugins.containsKey(plugin.getDescription().getName())) {
            return plugin.isEnabled();
        } else {
            return false;
        }
    }

    public void enablePlugin(Plugin plugin) {
        if (!plugin.isEnabled()) {
            try {
                for (Permission permission : plugin.getDescription().getPermissions()) {
                    this.addPermission(permission);
                }
                plugin.getPluginLoader().enablePlugin(plugin);
            } catch (Throwable e) {
                log.fatal("An error occurred while enabling the plugin {}, {}, {}",
                        plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getMain(), e);
                this.disablePlugin(plugin);
            }
        }
    }

    protected List parseYamlCommands(Plugin plugin) {
        List pluginCmds = new ArrayList<>();

        for (Map.Entry entry : plugin.getDescription().getCommands().entrySet()) {
            String key = (String) entry.getKey();
            Object data = entry.getValue();
            if (key.contains(":")) {
                log.fatal(this.server.getLanguage().tr("nukkit.plugin.commandError", new String[]{key, plugin.getDescription().getFullName()}));
                continue;
            }
            if (data instanceof Map) {
                PluginCommand newCmd = new PluginCommand<>(key, plugin);

                if (((Map) data).containsKey("description")) {
                    newCmd.setDescription((String) ((Map) data).get("description"));
                }

                if (((Map) data).containsKey("usage")) {
                    newCmd.setUsage((String) ((Map) data).get("usage"));
                }

                if (((Map) data).containsKey("aliases")) {
                    Object aliases = ((Map) data).get("aliases");
                    if (aliases instanceof List) {
                        List aliasList = new ArrayList<>();
                        for (String alias : (List) aliases) {
                            if (alias.contains(":")) {
                                log.fatal(this.server.getLanguage().tr("nukkit.plugin.aliasError", new String[]{alias, plugin.getDescription().getFullName()}));
                                continue;
                            }
                            aliasList.add(alias);
                        }

                        newCmd.setAliases(aliasList.toArray(EmptyArrays.EMPTY_STRINGS));
                    }
                }

                if (((Map) data).containsKey("permission")) {
                    newCmd.setPermission((String) ((Map) data).get("permission"));
                }

                if (((Map) data).containsKey("permission-message")) {
                    newCmd.setPermissionMessage((String) ((Map) data).get("permission-message"));
                }

                pluginCmds.add(newCmd);
            }
        }

        return pluginCmds;
    }

    @PowerNukkitDifference(info = "Makes sure the PowerNukkitPlugin is never disabled", since = "1.3.0.0-PN")
    @PowerNukkitXDifference(info = "Makes sure the PowerNukkitX Internal Plugin is never disabled", since = "1.19.60-r1")
    public void disablePlugins() {
        ListIterator plugins = new ArrayList<>(this.getPlugins().values()).listIterator(this.getPlugins().size());

        while (plugins.hasPrevious()) {
            Plugin previous = plugins.previous();
            if (previous != InternalPlugin.INSTANCE) {
                this.disablePlugin(previous);
            }
        }
    }

    public void disablePlugin(Plugin plugin) {
        if (InternalPlugin.INSTANCE == plugin) {
            throw new UnsupportedOperationException("The PowerNukkitX Internal plugin can't be disabled.");
        }

        if (plugin.isEnabled()) {
            try {
                plugin.getPluginLoader().disablePlugin(plugin);
            } catch (Exception e) {
                log.fatal("An error occurred while disabling the plugin {}, {}, {}",
                        plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getMain(), e);
            }

            this.server.getScheduler().cancelTask(plugin);
            HandlerList.unregisterAll(plugin);
            for (Permission permission : plugin.getDescription().getPermissions()) {
                this.removePermission(permission);
            }
        }
    }

    public void clearPlugins() {
        this.disablePlugins();
        this.plugins.clear();
        this.fileAssociations.clear();
        this.permissions.clear();
        this.defaultPerms.clear();
        this.defaultPermsOp.clear();
    }

    public void callEvent(Event event) {
        //Used for event listeners inside command blocks
        String eventName = event.getClass().getSimpleName();
        try {
            for (RegisteredListener registration : getEventListeners(event.getClass()).getRegisteredListeners()) {
                if (!registration.getPlugin().isEnabled()) {
                    continue;
                }

                try {
                    registration.callEvent(event);
                } catch (Exception e) {
                    log.error(this.server.getLanguage().tr("nukkit.plugin.eventError", event.getEventName(), registration.getPlugin().getDescription().getFullName(), e.getMessage(), registration.getListener().getClass().getName()), e);
                }
            }
        } catch (IllegalAccessException e) {
            log.error("An error has occurred while calling the event {}", event, e);
        }
    }

    public void registerEvents(Listener listener, Plugin plugin) {
        if (!plugin.isEnabled()) {
            throw new PluginException("Plugin attempted to register " + listener.getClass().getName() + " while not enabled");
        }

        Map, Set> ret = new HashMap<>();
        Set methods;
        try {
            Method[] publicMethods = listener.getClass().getMethods();
            Method[] privateMethods = listener.getClass().getDeclaredMethods();
            methods = new HashSet<>(publicMethods.length + privateMethods.length, 1.0f);
            Collections.addAll(methods, publicMethods);
            Collections.addAll(methods, privateMethods);
        } catch (NoClassDefFoundError e) {
            plugin.getLogger().error("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
            return;
        }

        for (final Method method : methods) {
            final EventHandler eh = method.getAnnotation(EventHandler.class);
            if (eh == null) continue;
            if (method.isBridge() || method.isSynthetic()) {
                continue;
            }
            final Class checkClass;

            if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
                plugin.getLogger().error(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
                continue;
            }

            final Class eventClass = checkClass.asSubclass(Event.class);
            method.setAccessible(true);

            for (Class clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
                // This loop checks for extending deprecated events
                if (clazz.getAnnotation(Deprecated.class) != null) {
                    if (Boolean.parseBoolean(String.valueOf(this.server.getConfig("settings.deprecated-verbose", true)))) {
                        log.warn(this.server.getLanguage().tr("nukkit.plugin.deprecatedEvent", plugin.getName(), clazz.getName(), listener.getClass().getName() + "." + method.getName() + "()"));
                    }
                    break;
                }
            }
            EventExecutor eventExecutor = MethodEventExecutor.compile(listener.getClass(), method);
            if (eventExecutor == null) {
                eventExecutor = new MethodEventExecutor(method);
                log.debug("Compile fast EventExecutor {} for {} failed!", eventClass.getName(), plugin.getName());
            }
            this.registerEvent(eventClass, listener, eh.priority(), eventExecutor, plugin, eh.ignoreCancelled());
        }
    }

    public void registerEvent(Class event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin) throws PluginException {
        this.registerEvent(event, listener, priority, executor, plugin, false);
    }

    public void registerEvent(Class event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin, boolean ignoreCancelled) throws PluginException {
        if (!plugin.isEnabled()) {
            throw new PluginException("Plugin attempted to register " + event + " while not enabled");
        }

        try {
            Timing timing = Timings.getPluginEventTiming(event, listener, executor, plugin);
            this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled, timing));
        } catch (IllegalAccessException e) {
            log.error("An error occurred while registering the event listener event:{}, listener:{} for plugin:{} version:{}",
                    event, listener, plugin.getDescription().getName(), plugin.getDescription().getVersion(), e);
        }
    }

    private HandlerList getEventListeners(Class type) throws IllegalAccessException {
        try {
            Method method = getRegistrationClass(type).getDeclaredMethod("getHandlers");
            method.setAccessible(true);
            return (HandlerList) method.invoke(null);
        } catch (NullPointerException e) {
            throw new IllegalArgumentException("getHandlers method in " + type.getName() + " was not static!", e);
        } catch (Exception e) {
            IllegalAccessException illegalAccessException = new IllegalAccessException(Utils.getExceptionMessage(e));
            illegalAccessException.addSuppressed(e);
            throw illegalAccessException;
        }
    }

    private Class getRegistrationClass(Class clazz) throws IllegalAccessException {
        try {
            clazz.getDeclaredMethod("getHandlers");
            return clazz;
        } catch (NoSuchMethodException e) {
            if (clazz.getSuperclass() != null
                    && !clazz.getSuperclass().equals(Event.class)
                    && Event.class.isAssignableFrom(clazz.getSuperclass())) {
                return getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
            } else {
                throw new IllegalAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlers method required!");
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy