BesuPluginContextImpl.java
/*
* Copyright ConsenSys AG.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.services;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import org.hyperledger.besu.plugin.BesuContext;
import org.hyperledger.besu.plugin.BesuPlugin;
import org.hyperledger.besu.plugin.services.BesuService;
import org.hyperledger.besu.plugin.services.PluginVersionsProvider;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** The Besu plugin context implementation. */
public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvider {
private static final Logger LOG = LoggerFactory.getLogger(BesuPluginContextImpl.class);
private enum Lifecycle {
/** Uninitialized lifecycle. */
UNINITIALIZED,
/** Registering lifecycle. */
REGISTERING,
/** Registered lifecycle. */
REGISTERED,
/** Before external services started lifecycle. */
BEFORE_EXTERNAL_SERVICES_STARTED,
/** Before external services finished lifecycle. */
BEFORE_EXTERNAL_SERVICES_FINISHED,
/** Before main loop started lifecycle. */
BEFORE_MAIN_LOOP_STARTED,
/** Before main loop finished lifecycle. */
BEFORE_MAIN_LOOP_FINISHED,
/** Stopping lifecycle. */
STOPPING,
/** Stopped lifecycle. */
STOPPED
}
private Lifecycle state = Lifecycle.UNINITIALIZED;
private final Map<Class<?>, ? super BesuService> serviceRegistry = new HashMap<>();
private final List<BesuPlugin> plugins = new ArrayList<>();
private final List<String> pluginVersions = new ArrayList<>();
final List<String> lines = new ArrayList<>();
/**
* Add service.
*
* @param <T> the type parameter
* @param serviceType the service type
* @param service the service
*/
@Override
public <T extends BesuService> void addService(final Class<T> serviceType, final T service) {
checkArgument(serviceType.isInterface(), "Services must be Java interfaces.");
checkArgument(
serviceType.isInstance(service),
"The service registered with a type must implement that type");
serviceRegistry.put(serviceType, service);
}
@SuppressWarnings("unchecked")
@Override
public <T extends BesuService> Optional<T> getService(final Class<T> serviceType) {
return Optional.ofNullable((T) serviceRegistry.get(serviceType));
}
/**
* Register plugins.
*
* @param pluginsDir the plugins dir
*/
public void registerPlugins(final Path pluginsDir) {
lines.add("Plugins:");
checkState(
state == Lifecycle.UNINITIALIZED,
"Besu plugins have already been registered. Cannot register additional plugins.");
final ClassLoader pluginLoader =
pluginDirectoryLoader(pluginsDir).orElse(this.getClass().getClassLoader());
state = Lifecycle.REGISTERING;
final ServiceLoader<BesuPlugin> serviceLoader =
ServiceLoader.load(BesuPlugin.class, pluginLoader);
int pluginsCount = 0;
for (final BesuPlugin plugin : serviceLoader) {
pluginsCount++;
try {
plugin.register(this);
LOG.info("Registered plugin of type {}.", plugin.getClass().getName());
String pluginVersion = getPluginVersion(plugin);
pluginVersions.add(pluginVersion);
lines.add(String.format("%s (%s)", plugin.getClass().getSimpleName(), pluginVersion));
} catch (final Exception e) {
LOG.error(
"Error registering plugin of type "
+ plugin.getClass().getName()
+ ", start and stop will not be called.",
e);
lines.add(String.format("ERROR %s", plugin.getClass().getSimpleName()));
continue;
}
plugins.add(plugin);
}
LOG.debug("Plugin registration complete.");
lines.add(
String.format(
"TOTAL = %d of %d plugins successfully loaded", plugins.size(), pluginsCount));
lines.add(String.format("from %s", pluginsDir.toAbsolutePath()));
state = Lifecycle.REGISTERED;
}
/**
* get the summary log, as a list of string lines
*
* @return the summary
*/
public List<String> getPluginsSummaryLog() {
return lines;
}
private String getPluginVersion(final BesuPlugin plugin) {
final Package pluginPackage = plugin.getClass().getPackage();
final String implTitle =
Optional.ofNullable(pluginPackage.getImplementationTitle())
.filter(Predicate.not(String::isBlank))
.orElse(plugin.getClass().getSimpleName());
final String implVersion =
Optional.ofNullable(pluginPackage.getImplementationVersion())
.filter(Predicate.not(String::isBlank))
.orElse("<Unknown Version>");
return implTitle + "/v" + implVersion;
}
/** Before external services. */
public void beforeExternalServices() {
checkState(
state == Lifecycle.REGISTERED,
"BesuContext should be in state %s but it was in %s",
Lifecycle.REGISTERED,
state);
state = Lifecycle.BEFORE_EXTERNAL_SERVICES_STARTED;
final Iterator<BesuPlugin> pluginsIterator = plugins.iterator();
while (pluginsIterator.hasNext()) {
final BesuPlugin plugin = pluginsIterator.next();
try {
plugin.beforeExternalServices();
LOG.debug(
"beforeExternalServices called on plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) {
LOG.error(
"Error calling `beforeExternalServices` on plugin of type "
+ plugin.getClass().getName()
+ ", stop will not be called.",
e);
pluginsIterator.remove();
}
}
LOG.debug("Plugin startup complete.");
state = Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED;
}
/** Start plugins. */
public void startPlugins() {
checkState(
state == Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED,
"BesuContext should be in state %s but it was in %s",
Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED,
state);
state = Lifecycle.BEFORE_MAIN_LOOP_STARTED;
final Iterator<BesuPlugin> pluginsIterator = plugins.iterator();
while (pluginsIterator.hasNext()) {
final BesuPlugin plugin = pluginsIterator.next();
try {
plugin.start();
LOG.debug("Started plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) {
LOG.error(
"Error starting plugin of type "
+ plugin.getClass().getName()
+ ", stop will not be called.",
e);
pluginsIterator.remove();
}
}
LOG.debug("Plugin startup complete.");
state = Lifecycle.BEFORE_MAIN_LOOP_FINISHED;
}
/** Stop plugins. */
public void stopPlugins() {
checkState(
state == Lifecycle.BEFORE_MAIN_LOOP_FINISHED,
"BesuContext should be in state %s but it was in %s",
Lifecycle.BEFORE_MAIN_LOOP_FINISHED,
state);
state = Lifecycle.STOPPING;
for (final BesuPlugin plugin : plugins) {
try {
plugin.stop();
LOG.debug("Stopped plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) {
LOG.error("Error stopping plugin of type " + plugin.getClass().getName(), e);
}
}
LOG.debug("Plugin shutdown complete.");
state = Lifecycle.STOPPED;
}
@Override
public Collection<String> getPluginVersions() {
return Collections.unmodifiableList(pluginVersions);
}
private static URL pathToURIOrNull(final Path p) {
try {
return p.toUri().toURL();
} catch (final MalformedURLException e) {
return null;
}
}
/**
* Gets plugins.
*
* @return the plugins
*/
@VisibleForTesting
List<BesuPlugin> getPlugins() {
return Collections.unmodifiableList(plugins);
}
private Optional<ClassLoader> pluginDirectoryLoader(final Path pluginsDir) {
if (pluginsDir != null && pluginsDir.toFile().isDirectory()) {
LOG.debug("Searching for plugins in {}", pluginsDir.toAbsolutePath());
try (final Stream<Path> pluginFilesList = Files.list(pluginsDir)) {
final URL[] pluginJarURLs =
pluginFilesList
.filter(p -> p.getFileName().toString().endsWith(".jar"))
.map(BesuPluginContextImpl::pathToURIOrNull)
.toArray(URL[]::new);
return Optional.of(new URLClassLoader(pluginJarURLs, this.getClass().getClassLoader()));
} catch (final MalformedURLException e) {
LOG.error("Error converting files to URLs, could not load plugins", e);
} catch (final IOException e) {
LOG.error("Error enumerating plugins, could not load plugins", e);
}
} else {
LOG.debug("Plugin directory does not exist, skipping registration. - {}", pluginsDir);
}
return Optional.empty();
}
/**
* Gets named plugins.
*
* @return the named plugins
*/
public Map<String, BesuPlugin> getNamedPlugins() {
return plugins.stream()
.filter(plugin -> plugin.getName().isPresent())
.collect(Collectors.toMap(plugin -> plugin.getName().get(), plugin -> plugin, (a, b) -> b));
}
}