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

org.elasticsearch.plugins.PluginsService Maven / Gradle / Ivy

There is a newer version: 8.14.1
Show newest version
/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.plugins;

import org.apache.lucene.analysis.util.CharFilterFactory;
import org.apache.lucene.analysis.util.TokenFilterFactory;
import org.apache.lucene.analysis.util.TokenizerFactory;
import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.DocValuesFormat;
import org.apache.lucene.codecs.PostingsFormat;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.cluster.node.info.PluginsAndModules;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.component.LifecycleComponent;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexModule;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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 static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;

/**
 *
 */
public class PluginsService extends AbstractComponent {

    /**
     * We keep around a list of plugins and modules
     */
    private final List> plugins;
    private final PluginsAndModules info;

    private final Map> onModuleReferences;

    static class OnModuleReference {
        public final Class moduleClass;
        public final Method onModuleMethod;

        OnModuleReference(Class moduleClass, Method onModuleMethod) {
            this.moduleClass = moduleClass;
            this.onModuleMethod = onModuleMethod;
        }
    }

    /**
     * Constructs a new PluginService
     * @param settings The settings of the system
     * @param modulesDirectory The directory modules exist in, or null if modules should not be loaded from the filesystem
     * @param pluginsDirectory The directory plugins exist in, or null if plugins should not be loaded from the filesystem
     * @param classpathPlugins Plugins that exist in the classpath which should be loaded
     */
    public PluginsService(Settings settings, Path modulesDirectory, Path pluginsDirectory, Collection> classpathPlugins) {
        super(settings);
        info = new PluginsAndModules();

        List> pluginsLoaded = new ArrayList<>();

        // first we load plugins that are on the classpath. this is for tests and transport clients
        for (Class pluginClass : classpathPlugins) {
            Plugin plugin = loadPlugin(pluginClass, settings);
            PluginInfo pluginInfo = new PluginInfo(plugin.name(), plugin.description(), false, "NA", true, pluginClass.getName(), false);
            if (logger.isTraceEnabled()) {
                logger.trace("plugin loaded from classpath [{}]", pluginInfo);
            }
            pluginsLoaded.add(new Tuple<>(pluginInfo, plugin));
            info.addPlugin(pluginInfo);
        }

        // load modules
        if (modulesDirectory != null) {
            try {
                List bundles = getModuleBundles(modulesDirectory);
                List> loaded = loadBundles(bundles);
                pluginsLoaded.addAll(loaded);
                for (Tuple module : loaded) {
                    info.addModule(module.v1());
                }
            } catch (IOException ex) {
                throw new IllegalStateException("Unable to initialize modules", ex);
            }
        }

        // now, find all the ones that are in plugins/
        if (pluginsDirectory != null) {
            try {
                List bundles = getPluginBundles(pluginsDirectory);
                List> loaded = loadBundles(bundles);
                pluginsLoaded.addAll(loaded);
                for (Tuple plugin : loaded) {
                    info.addPlugin(plugin.v1());
                }
            } catch (IOException ex) {
                throw new IllegalStateException("Unable to initialize plugins", ex);
            }
        }

        plugins = Collections.unmodifiableList(pluginsLoaded);

        // We need to build a List of jvm and site plugins for checking mandatory plugins
        Map jvmPlugins = new HashMap<>();
        List sitePlugins = new ArrayList<>();

        for (Tuple tuple : plugins) {
            PluginInfo info = tuple.v1();
            if (info.isJvm()) {
                jvmPlugins.put(info.getName(), tuple.v2());
            }
            if (info.isSite()) {
                sitePlugins.add(info.getName());
            }
        }

        // Checking expected plugins
        String[] mandatoryPlugins = settings.getAsArray("plugin.mandatory", null);
        if (mandatoryPlugins != null) {
            Set missingPlugins = new HashSet<>();
            for (String mandatoryPlugin : mandatoryPlugins) {
                if (!jvmPlugins.containsKey(mandatoryPlugin) && !sitePlugins.contains(mandatoryPlugin) && !missingPlugins.contains(mandatoryPlugin)) {
                    missingPlugins.add(mandatoryPlugin);
                }
            }
            if (!missingPlugins.isEmpty()) {
                throw new ElasticsearchException("Missing mandatory plugins [" + Strings.collectionToDelimitedString(missingPlugins, ", ") + "]");
            }
        }

        // we don't log jars in lib/ we really shouldnt log modules,
        // but for now: just be transparent so we can debug any potential issues
        Set moduleNames = new HashSet<>();
        Set jvmPluginNames = new HashSet<>();
        for (PluginInfo moduleInfo : info.getModuleInfos()) {
            moduleNames.add(moduleInfo.getName());
        }
        for (PluginInfo pluginInfo : info.getPluginInfos()) {
            jvmPluginNames.add(pluginInfo.getName());
        }

        logger.info("modules {}, plugins {}, sites {}", moduleNames, jvmPluginNames, sitePlugins);

        Map> onModuleReferences = new HashMap<>();
        for (Plugin plugin : jvmPlugins.values()) {
            List list = new ArrayList<>();
            for (Method method : plugin.getClass().getMethods()) {
                if (!method.getName().equals("onModule")) {
                    continue;
                }
                // this is a deprecated final method, so all Plugin subclasses have it
                if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(IndexModule.class)) {
                    continue;
                }
                if (method.getParameterTypes().length == 0 || method.getParameterTypes().length > 1) {
                    logger.warn("Plugin: {} implementing onModule with no parameters or more than one parameter", plugin.name());
                    continue;
                }
                Class moduleClass = method.getParameterTypes()[0];
                if (!Module.class.isAssignableFrom(moduleClass)) {
                    logger.warn("Plugin: {} implementing onModule by the type is not of Module type {}", plugin.name(), moduleClass);
                    continue;
                }
                list.add(new OnModuleReference(moduleClass, method));
            }
            if (!list.isEmpty()) {
                onModuleReferences.put(plugin, list);
            }
        }
        this.onModuleReferences = Collections.unmodifiableMap(onModuleReferences);
    }

    private List> plugins() {
        return plugins;
    }

    public void processModules(Iterable modules) {
        for (Module module : modules) {
            processModule(module);
        }
    }

    public void processModule(Module module) {
        for (Tuple plugin : plugins()) {
            // see if there are onModule references
            List references = onModuleReferences.get(plugin.v2());
            if (references != null) {
                for (OnModuleReference reference : references) {
                    if (reference.moduleClass.isAssignableFrom(module.getClass())) {
                        try {
                            reference.onModuleMethod.invoke(plugin.v2(), module);
                        } catch (IllegalAccessException | InvocationTargetException e) {
                            logger.warn("plugin {}, failed to invoke custom onModule method", e, plugin.v2().name());
                            throw new ElasticsearchException("failed to invoke onModule", e);
                        } catch (Exception e) {
                            logger.warn("plugin {}, failed to invoke custom onModule method", e, plugin.v2().name());
                            throw e;
                        }
                    }
                }
            }
        }
    }

    public Settings updatedSettings() {
        Map foundSettings = new HashMap<>();
        final Settings.Builder builder = Settings.settingsBuilder();
        for (Tuple plugin : plugins) {
            Settings settings = plugin.v2().additionalSettings();
            for (String setting : settings.getAsMap().keySet()) {
                String oldPlugin = foundSettings.put(setting, plugin.v1().getName());
                if (oldPlugin != null) {
                    throw new IllegalArgumentException("Cannot have additional setting [" + setting + "] " +
                        "in plugin [" + plugin.v1().getName() + "], already added in plugin [" + oldPlugin + "]");
                }
            }
            builder.put(settings);
        }
        return builder.put(this.settings).build();
    }

    public Collection nodeModules() {
        List modules = new ArrayList<>();
        for (Tuple plugin : plugins) {
            modules.addAll(plugin.v2().nodeModules());
        }
        return modules;
    }

    public Collection> nodeServices() {
        List> services = new ArrayList<>();
        for (Tuple plugin : plugins) {
            services.addAll(plugin.v2().nodeServices());
        }
        return services;
    }

    public Collection indexModules(Settings indexSettings) {
        List modules = new ArrayList<>();
        for (Tuple plugin : plugins) {
            modules.addAll(plugin.v2().indexModules(indexSettings));
        }
        return modules;
    }

    public Collection> indexServices() {
        List> services = new ArrayList<>();
        for (Tuple plugin : plugins) {
            services.addAll(plugin.v2().indexServices());
        }
        return services;
    }

    public Collection shardModules(Settings indexSettings) {
        List modules = new ArrayList<>();
        for (Tuple plugin : plugins) {
            modules.addAll(plugin.v2().shardModules(indexSettings));
        }
        return modules;
    }

    public Collection> shardServices() {
        List> services = new ArrayList<>();
        for (Tuple plugin : plugins) {
            services.addAll(plugin.v2().shardServices());
        }
        return services;
    }

    /**
     * Get information about plugins and modules
     */
    public PluginsAndModules info() {
        return info;
    }

    // a "bundle" is a group of plugins in a single classloader
    // really should be 1-1, but we are not so fortunate
    static class Bundle {
        List plugins = new ArrayList<>();
        List urls = new ArrayList<>();
    }

    // similar in impl to getPluginBundles, but DO NOT try to make them share code.
    // we don't need to inherit all the leniency, and things are different enough.
    static List getModuleBundles(Path modulesDirectory) throws IOException {
        // damn leniency
        if (Files.notExists(modulesDirectory)) {
            return Collections.emptyList();
        }
        List bundles = new ArrayList<>();
        try (DirectoryStream stream = Files.newDirectoryStream(modulesDirectory)) {
            for (Path module : stream) {
                if (FileSystemUtils.isHidden(module)) {
                    continue; // skip over .DS_Store etc
                }
                PluginInfo info = PluginInfo.readFromProperties(module);
                if (!info.isJvm()) {
                    throw new IllegalStateException("modules must be jvm plugins: " + info);
                }
                if (!info.isIsolated()) {
                    throw new IllegalStateException("modules must be isolated: " + info);
                }
                Bundle bundle = new Bundle();
                bundle.plugins.add(info);
                // gather urls for jar files
                try (DirectoryStream jarStream = Files.newDirectoryStream(module, "*.jar")) {
                    for (Path jar : jarStream) {
                        // normalize with toRealPath to get symlinks out of our hair
                        bundle.urls.add(jar.toRealPath().toUri().toURL());
                    }
                }
                bundles.add(bundle);
            }
        }
        return bundles;
    }

    static List getPluginBundles(Path pluginsDirectory) throws IOException {
        ESLogger logger = Loggers.getLogger(PluginsService.class);

        // TODO: remove this leniency, but tests bogusly rely on it
        if (!isAccessibleDirectory(pluginsDirectory, logger)) {
            return Collections.emptyList();
        }

        List bundles = new ArrayList<>();
        // a special purgatory for plugins that directly depend on each other
        bundles.add(new Bundle());

        try (DirectoryStream stream = Files.newDirectoryStream(pluginsDirectory)) {
            for (Path plugin : stream) {
                if (FileSystemUtils.isHidden(plugin)) {
                    logger.trace("--- skip hidden plugin file[{}]", plugin.toAbsolutePath());
                    continue;
                }
                logger.trace("--- adding plugin [{}]", plugin.toAbsolutePath());
                final PluginInfo info;
                try {
                    info = PluginInfo.readFromProperties(plugin);
                } catch (IOException e) {
                    throw new IllegalStateException("Could not load plugin descriptor for existing plugin ["
                        + plugin.getFileName() + "]. Was the plugin built before 2.0?", e);
                }

                List urls = new ArrayList<>();
                if (info.isJvm()) {
                    // a jvm plugin: gather urls for jar files
                    try (DirectoryStream jarStream = Files.newDirectoryStream(plugin, "*.jar")) {
                        for (Path jar : jarStream) {
                            // normalize with toRealPath to get symlinks out of our hair
                            urls.add(jar.toRealPath().toUri().toURL());
                        }
                    }
                }
                final Bundle bundle;
                if (info.isJvm() && info.isIsolated() == false) {
                    bundle = bundles.get(0); // purgatory
                } else {
                    bundle = new Bundle();
                    bundles.add(bundle);
                }
                bundle.plugins.add(info);
                bundle.urls.addAll(urls);
            }
        }

        return bundles;
    }

    private List> loadBundles(List bundles) {
        List> plugins = new ArrayList<>();

        for (Bundle bundle : bundles) {
            // jar-hell check the bundle against the parent classloader
            // pluginmanager does it, but we do it again, in case lusers mess with jar files manually
            try {
                final List jars = new ArrayList<>();
                jars.addAll(Arrays.asList(JarHell.parseClassPath()));
                jars.addAll(bundle.urls);
                JarHell.checkJarHell(jars.toArray(new URL[0]));
            } catch (Exception e) {
                throw new IllegalStateException("failed to load bundle " + bundle.urls + " due to jar hell", e);
            }

            // create a child to load the plugins in this bundle
            ClassLoader loader = URLClassLoader.newInstance(bundle.urls.toArray(new URL[0]), getClass().getClassLoader());
            for (PluginInfo pluginInfo : bundle.plugins) {
                final Plugin plugin;
                if (pluginInfo.isJvm()) {
                    // reload lucene SPI with any new services from the plugin
                    reloadLuceneSPI(loader);
                    Class pluginClass = loadPluginClass(pluginInfo.getClassname(), loader);
                    plugin = loadPlugin(pluginClass, settings);
                } else {
                    plugin = new SitePlugin(pluginInfo.getName(), pluginInfo.getDescription());
                }
                plugins.add(new Tuple<>(pluginInfo, plugin));
            }
        }

        return Collections.unmodifiableList(plugins);
    }

    /**
     * Reloads all Lucene SPI implementations using the new classloader.
     * This method must be called after the new classloader has been created to
     * register the services for use.
     */
    static void reloadLuceneSPI(ClassLoader loader) {
        // do NOT change the order of these method calls!

        // Codecs:
        PostingsFormat.reloadPostingsFormats(loader);
        DocValuesFormat.reloadDocValuesFormats(loader);
        Codec.reloadCodecs(loader);
        // Analysis:
        CharFilterFactory.reloadCharFilters(loader);
        TokenFilterFactory.reloadTokenFilters(loader);
        TokenizerFactory.reloadTokenizers(loader);
    }

    private Class loadPluginClass(String className, ClassLoader loader) {
        try {
            return loader.loadClass(className).asSubclass(Plugin.class);
        } catch (ClassNotFoundException e) {
            throw new ElasticsearchException("Could not find plugin class [" + className + "]", e);
        }
    }

    private Plugin loadPlugin(Class pluginClass, Settings settings) {
        try {
            try {
                return pluginClass.getConstructor(Settings.class).newInstance(settings);
            } catch (NoSuchMethodException e) {
                try {
                    return pluginClass.getConstructor().newInstance();
                } catch (NoSuchMethodException e1) {
                    throw new ElasticsearchException("No constructor for [" + pluginClass + "]. A plugin class must " +
                        "have either an empty default constructor or a single argument constructor accepting a " +
                        "Settings instance");
                }
            }
        } catch (Throwable e) {
            throw new ElasticsearchException("Failed to load plugin class [" + pluginClass.getName() + "]", e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy