Enhanced control over plugins registration (#6700)

Signed-off-by: Gabriel-Trintinalia <gabriel.trintinalia@consensys.net>
pull/6973/head
Gabriel-Trintinalia 6 months ago committed by GitHub
parent 61432831d5
commit a1f73d925e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 4
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java
  3. 142
      acceptance-tests/test-plugins/src/test/java/org/hyperledger/besu/services/BesuPluginContextImplTest.java
  4. 114
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  5. 3
      besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
  6. 53
      besu/src/main/java/org/hyperledger/besu/cli/converter/PluginInfoConverter.java
  7. 63
      besu/src/main/java/org/hyperledger/besu/cli/options/stable/PluginsConfigurationOptions.java
  8. 57
      besu/src/main/java/org/hyperledger/besu/cli/util/CommandLineUtils.java
  9. 30
      besu/src/main/java/org/hyperledger/besu/cli/util/ConfigDefaultValueProviderStrategy.java
  10. 224
      besu/src/main/java/org/hyperledger/besu/services/BesuPluginContextImpl.java
  11. 110
      besu/src/test/java/org/hyperledger/besu/cli/CommandLineUtilsDefaultsTest.java
  12. 1
      besu/src/test/java/org/hyperledger/besu/cli/CommandLineUtilsTest.java
  13. 39
      besu/src/test/java/org/hyperledger/besu/cli/util/ConfigDefaultValueProviderStrategyTest.java
  14. 92
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/plugins/PluginConfiguration.java
  15. 37
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/plugins/PluginInfo.java
  16. 2
      plugin-api/build.gradle
  17. 20
      plugin-api/src/main/java/org/hyperledger/besu/plugin/BesuPlugin.java

@ -41,6 +41,8 @@
- Expose bad block events via the BesuEvents plugin API [#6848](https://github.com/hyperledger/besu/pull/6848)
- Add RPC errors metric [#6919](https://github.com/hyperledger/besu/pull/6919/)
- Add `rlp decode` subcommand to decode IBFT/QBFT extraData to validator list [#6895](https://github.com/hyperledger/besu/pull/6895)
- Allow users to specify which plugins are registered [#6700](https://github.com/hyperledger/besu/pull/6700)
### Bug fixes
- Fix txpool dump/restore race condition [#6665](https://github.com/hyperledger/besu/pull/6665)

@ -31,6 +31,7 @@ import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.api.ApiConfiguration;
import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration;
import org.hyperledger.besu.ethereum.core.ImmutableMiningParameters;
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@ -141,7 +142,8 @@ public class ThreadBesuNodeRunner implements BesuNodeRunner {
besuPluginContext.addService(PermissioningService.class, permissioningService);
besuPluginContext.addService(PrivacyPluginService.class, new PrivacyPluginServiceImpl());
besuPluginContext.registerPlugins(pluginsPath);
besuPluginContext.registerPlugins(new PluginConfiguration(pluginsPath));
commandLine.parseArgs(node.getConfiguration().getExtraCLIOptions().toArray(new String[0]));
// register built-in plugins

@ -16,24 +16,31 @@ package org.hyperledger.besu.services;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.ethereum.core.plugins.PluginInfo;
import org.hyperledger.besu.plugin.BesuPlugin;
import org.hyperledger.besu.tests.acceptance.plugins.TestBesuEventsPlugin;
import org.hyperledger.besu.tests.acceptance.plugins.TestPicoCLIPlugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class BesuPluginContextImplTest {
private static final Path DEFAULT_PLUGIN_DIRECTORY = Paths.get(".");
private BesuPluginContextImpl contextImpl;
@BeforeAll
public static void createFakePluginDir() throws IOException {
@ -49,16 +56,20 @@ public class BesuPluginContextImplTest {
System.clearProperty("testPicoCLIPlugin.testOption");
}
@BeforeEach
void setup() {
contextImpl = new BesuPluginContextImpl();
}
@Test
public void verifyEverythingGoesSmoothly() {
final BesuPluginContextImpl contextImpl = new BesuPluginContextImpl();
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(new PluginConfiguration(DEFAULT_PLUGIN_DIRECTORY));
assertThat(contextImpl.getRegisteredPlugins()).isNotEmpty();
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).isNotEmpty();
final Optional<TestPicoCLIPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
Assertions.assertThat(testPluginOptional).isPresent();
final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
@ -72,33 +83,34 @@ public class BesuPluginContextImplTest {
@Test
public void registrationErrorsHandledSmoothly() {
final BesuPluginContextImpl contextImpl = new BesuPluginContextImpl();
System.setProperty("testPicoCLIPlugin.testOption", "FAILREGISTER");
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(new PluginConfiguration(DEFAULT_PLUGIN_DIRECTORY));
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.beforeExternalServices();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.startPlugins();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.stopPlugins();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
public void startErrorsHandledSmoothly() {
final BesuPluginContextImpl contextImpl = new BesuPluginContextImpl();
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTART");
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).extracting("class").contains(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(new PluginConfiguration(DEFAULT_PLUGIN_DIRECTORY));
assertThat(contextImpl.getRegisteredPlugins())
.extracting("class")
.contains(TestPicoCLIPlugin.class);
final Optional<TestPicoCLIPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
@ -106,22 +118,24 @@ public class BesuPluginContextImplTest {
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstart");
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.stopPlugins();
assertThat(contextImpl.getPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
public void stopErrorsHandledSmoothly() {
final BesuPluginContextImpl contextImpl = new BesuPluginContextImpl();
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTOP");
assertThat(contextImpl.getPlugins()).isEmpty();
contextImpl.registerPlugins(new File(".").toPath());
assertThat(contextImpl.getPlugins()).extracting("class").contains(TestPicoCLIPlugin.class);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(new PluginConfiguration(DEFAULT_PLUGIN_DIRECTORY));
assertThat(contextImpl.getRegisteredPlugins())
.extracting("class")
.contains(TestPicoCLIPlugin.class);
final Optional<TestPicoCLIPlugin> testPluginOptional = findTestPlugin(contextImpl.getPlugins());
final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
@ -136,9 +150,8 @@ public class BesuPluginContextImplTest {
@Test
public void lifecycleExceptions() throws Throwable {
final BesuPluginContextImpl contextImpl = new BesuPluginContextImpl();
final ThrowableAssert.ThrowingCallable registerPlugins =
() -> contextImpl.registerPlugins(new File(".").toPath());
() -> contextImpl.registerPlugins(new PluginConfiguration(DEFAULT_PLUGIN_DIRECTORY));
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins);
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins);
@ -158,9 +171,74 @@ public class BesuPluginContextImplTest {
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins);
}
private Optional<TestPicoCLIPlugin> findTestPlugin(final List<BesuPlugin> plugins) {
@Test
public void shouldRegisterAllPluginsWhenNoPluginsOption() {
final PluginConfiguration config = createConfigurationForAllPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config);
final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
}
@Test
public void shouldRegisterOnlySpecifiedPluginWhenPluginsOptionIsSet() {
final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin");
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config);
final Optional<TestPicoCLIPlugin> requestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(requestedPlugin).isPresent();
assertThat(requestedPlugin.get().getState()).isEqualTo("registered");
final Optional<TestPicoCLIPlugin> nonRequestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class);
assertThat(nonRequestedPlugin).isEmpty();
}
@Test
public void shouldNotRegisterUnspecifiedPluginsWhenPluginsOptionIsSet() {
final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin");
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config);
final Optional<TestPicoCLIPlugin> nonRequestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class);
assertThat(nonRequestedPlugin).isEmpty();
}
@Test
void shouldThrowExceptionIfExplicitlySpecifiedPluginNotFound() {
PluginConfiguration config = createConfigurationForSpecificPlugin("NonExistentPlugin");
String exceptionMessage =
assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins(config))
.getMessage();
final String expectedMessage =
"The following requested plugins were not found: NonExistentPlugin";
assertThat(exceptionMessage).isEqualTo(expectedMessage);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
}
private PluginConfiguration createConfigurationForAllPlugins() {
return new PluginConfiguration(null, DEFAULT_PLUGIN_DIRECTORY);
}
private PluginConfiguration createConfigurationForSpecificPlugin(final String pluginName) {
return new PluginConfiguration(List.of(new PluginInfo(pluginName)), DEFAULT_PLUGIN_DIRECTORY);
}
private Optional<TestPicoCLIPlugin> findTestPlugin(
final List<BesuPlugin> plugins, final Class<?> type) {
return plugins.stream()
.filter(p -> p instanceof TestPicoCLIPlugin)
.filter(p -> type.equals(p.getClass()))
.map(p -> (TestPicoCLIPlugin) p)
.findFirst();
}

@ -58,6 +58,7 @@ import org.hyperledger.besu.cli.options.stable.LoggingLevelOption;
import org.hyperledger.besu.cli.options.stable.NodePrivateKeyFileOption;
import org.hyperledger.besu.cli.options.stable.P2PTLSConfigOptions;
import org.hyperledger.besu.cli.options.stable.PermissionsOptions;
import org.hyperledger.besu.cli.options.stable.PluginsConfigurationOptions;
import org.hyperledger.besu.cli.options.stable.RpcWebsocketOptions;
import org.hyperledger.besu.cli.options.unstable.ChainPruningOptions;
import org.hyperledger.besu.cli.options.unstable.DnsOptions;
@ -85,7 +86,7 @@ import org.hyperledger.besu.cli.subcommands.rlp.RLPSubCommand;
import org.hyperledger.besu.cli.subcommands.storage.StorageSubCommand;
import org.hyperledger.besu.cli.util.BesuCommandCustomFactory;
import org.hyperledger.besu.cli.util.CommandLineUtils;
import org.hyperledger.besu.cli.util.ConfigOptionSearchAndRunHandler;
import org.hyperledger.besu.cli.util.ConfigDefaultValueProviderStrategy;
import org.hyperledger.besu.cli.util.VersionProvider;
import org.hyperledger.besu.components.BesuComponent;
import org.hyperledger.besu.config.CheckpointConfigOptions;
@ -121,6 +122,7 @@ import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.MiningParametersMetrics;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.VersionMetadata;
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SyncMode;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@ -876,6 +878,10 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
@Mixin private PkiBlockCreationOptions pkiBlockCreationOptions;
// Plugins Configuration Option Group
@CommandLine.ArgGroup(validate = false)
PluginsConfigurationOptions pluginsConfigurationOptions = new PluginsConfigurationOptions();
private EthNetworkConfig ethNetworkConfig;
private JsonRpcConfiguration jsonRpcConfiguration;
private JsonRpcConfiguration engineJsonRpcConfiguration;
@ -1020,6 +1026,16 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
* @param args arguments to Besu command
* @return success or failure exit code.
*/
/**
* Parses command line arguments and configures the application accordingly.
*
* @param resultHandler The strategy to handle the execution result.
* @param parameterExceptionHandler Handler for exceptions related to command line parameters.
* @param executionExceptionHandler Handler for exceptions during command execution.
* @param in The input stream for commands.
* @param args The command line arguments.
* @return The execution result status code.
*/
public int parse(
final IExecutionStrategy resultHandler,
final BesuParameterExceptionHandler parameterExceptionHandler,
@ -1027,9 +1043,24 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
final InputStream in,
final String... args) {
toCommandLine();
initializeCommandLineSettings(in);
// use terminal width for usage message
// Create the execution strategy chain.
final IExecutionStrategy executeTask = createExecuteTask(resultHandler);
final IExecutionStrategy pluginRegistrationTask = createPluginRegistrationTask(executeTask);
final IExecutionStrategy setDefaultValueProviderTask =
createDefaultValueProviderTask(pluginRegistrationTask);
// 1- Config default value provider
// 2- Register plugins
// 3- Execute command
return executeCommandLine(
setDefaultValueProviderTask, parameterExceptionHandler, executionExceptionHandler, args);
}
private void initializeCommandLineSettings(final InputStream in) {
toCommandLine();
// Automatically adjust the width of usage messages to the terminal width.
commandLine.getCommandSpec().usageMessage().autoWidth(true);
handleStableOptions();
@ -1037,11 +1068,51 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
registerConverters();
handleUnstableOptions();
preparePlugins();
}
final int exitCode =
parse(resultHandler, executionExceptionHandler, parameterExceptionHandler, args);
private IExecutionStrategy createExecuteTask(final IExecutionStrategy nextStep) {
return parseResult -> {
commandLine.setExecutionStrategy(nextStep);
// At this point we don't allow unmatched options since plugins were already registered
commandLine.setUnmatchedArgumentsAllowed(false);
return commandLine.execute(parseResult.originalArgs().toArray(new String[0]));
};
}
return exitCode;
private IExecutionStrategy createPluginRegistrationTask(final IExecutionStrategy nextStep) {
return parseResult -> {
PluginConfiguration configuration =
PluginsConfigurationOptions.fromCommandLine(parseResult.commandSpec().commandLine());
besuPluginContext.registerPlugins(configuration);
commandLine.setExecutionStrategy(nextStep);
return commandLine.execute(parseResult.originalArgs().toArray(new String[0]));
};
}
private IExecutionStrategy createDefaultValueProviderTask(final IExecutionStrategy nextStep) {
return new ConfigDefaultValueProviderStrategy(nextStep, environment);
}
/**
* Executes the command line with the provided execution strategy and exception handlers.
*
* @param executionStrategy The execution strategy to use.
* @param args The command line arguments.
* @return The execution result status code.
*/
private int executeCommandLine(
final IExecutionStrategy executionStrategy,
final BesuParameterExceptionHandler parameterExceptionHandler,
final BesuExecutionExceptionHandler executionExceptionHandler,
final String... args) {
return commandLine
.setExecutionStrategy(executionStrategy)
.setParameterExceptionHandler(parameterExceptionHandler)
.setExecutionExceptionHandler(executionExceptionHandler)
// As this happens before the plugins registration and plugins can add options, we must
// allow unmatched options
.setUnmatchedArgumentsAllowed(true)
.execute(args);
}
/** Used by Dagger to parse all options into a commandline instance. */
@ -1208,8 +1279,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
rocksDBPlugin.register(besuPluginContext);
new InMemoryStoragePlugin().register(besuPluginContext);
besuPluginContext.registerPlugins(pluginsDir());
metricCategoryRegistry
.getMetricCategories()
.forEach(metricCategoryConverter::addRegistryCategory);
@ -1235,26 +1304,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
return KeyPairUtil.loadKeyPair(resolveNodePrivateKeyFile(nodePrivateKeyFile));
}
private int parse(
final CommandLine.IExecutionStrategy resultHandler,
final BesuExecutionExceptionHandler besuExecutionExceptionHandler,
final BesuParameterExceptionHandler besuParameterExceptionHandler,
final String... args) {
// Create a handler that will search for a config file option and use it for
// default values
// and eventually it will run regular parsing of the remaining options.
final ConfigOptionSearchAndRunHandler configParsingHandler =
new ConfigOptionSearchAndRunHandler(
resultHandler, besuParameterExceptionHandler, environment);
return commandLine
.setExecutionStrategy(configParsingHandler)
.setParameterExceptionHandler(besuParameterExceptionHandler)
.setExecutionExceptionHandler(besuExecutionExceptionHandler)
.execute(args);
}
private void preSynchronization() {
preSynchronizationTaskRunner.runTasks(besuController);
}
@ -2382,15 +2431,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
return dataPath.toAbsolutePath();
}
private Path pluginsDir() {
final String pluginsDir = System.getProperty("besu.plugins.dir");
if (pluginsDir == null) {
return new File(System.getProperty("besu.home", "."), "plugins").toPath();
} else {
return new File(pluginsDir).toPath();
}
}
private SecurityModule securityModule() {
return securityModuleService
.getByName(securityModuleName)

@ -125,6 +125,9 @@ public interface DefaultCommandValues {
/** The Default tls protocols. */
List<String> DEFAULT_TLS_PROTOCOLS = List.of("TLSv1.3", "TLSv1.2");
/** The constant DEFAULT_PLUGINS_OPTION_NAME. */
String DEFAULT_PLUGINS_OPTION_NAME = "--plugins";
/**
* Gets default besu data path.
*

@ -0,0 +1,53 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.cli.converter;
import org.hyperledger.besu.ethereum.core.plugins.PluginInfo;
import java.util.List;
import java.util.stream.Stream;
import picocli.CommandLine;
/**
* Converts a comma-separated string into a list of {@link PluginInfo} objects. This converter is
* intended for use with PicoCLI to process command line arguments that specify plugin information.
*/
public class PluginInfoConverter implements CommandLine.ITypeConverter<List<PluginInfo>> {
/**
* Converts a comma-separated string into a list of {@link PluginInfo}.
*
* @param value The comma-separated string representing plugin names.
* @return A list of {@link PluginInfo} objects created from the provided string.
*/
@Override
public List<PluginInfo> convert(final String value) {
if (value == null || value.isBlank()) {
return List.of();
}
return Stream.of(value.split(",")).map(String::trim).map(this::toPluginInfo).toList();
}
/**
* Creates a {@link PluginInfo} object from a plugin name.
*
* @param pluginName The name of the plugin.
* @return A {@link PluginInfo} object representing the plugin.
*/
private PluginInfo toPluginInfo(final String pluginName) {
return new PluginInfo(pluginName);
}
}

@ -0,0 +1,63 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.cli.options.stable;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_OPTION_NAME;
import org.hyperledger.besu.cli.converter.PluginInfoConverter;
import org.hyperledger.besu.cli.options.CLIOptions;
import org.hyperledger.besu.cli.util.CommandLineUtils;
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.ethereum.core.plugins.PluginInfo;
import java.util.List;
import picocli.CommandLine;
/** The Plugins Options options. */
public class PluginsConfigurationOptions implements CLIOptions<PluginConfiguration> {
@CommandLine.Option(
names = {DEFAULT_PLUGINS_OPTION_NAME},
description = "Comma-separated list of plugin names",
split = ",",
hidden = true,
converter = PluginInfoConverter.class,
arity = "1..*")
private List<PluginInfo> plugins;
@Override
public PluginConfiguration toDomainObject() {
return new PluginConfiguration(plugins);
}
@Override
public List<String> getCLIOptions() {
return CommandLineUtils.getCLIOptions(this, new PluginsConfigurationOptions());
}
/**
* Constructs a {@link PluginConfiguration} instance based on the command line options.
*
* @param commandLine The command line instance containing parsed options.
* @return A new {@link PluginConfiguration} instance.
*/
public static PluginConfiguration fromCommandLine(final CommandLine commandLine) {
List<PluginInfo> plugins =
CommandLineUtils.getOptionValueOrDefault(
commandLine, DEFAULT_PLUGINS_OPTION_NAME, new PluginInfoConverter());
return new PluginConfiguration(plugins);
}
}

@ -264,4 +264,61 @@ public class CommandLineUtils {
.filter(optionSpec -> Arrays.stream(optionSpec.names()).anyMatch(optionName::equals))
.anyMatch(CommandLineUtils::isOptionSet);
}
/**
* Retrieves the value of a specified command line option, converting it to its appropriate type,
* or returns the default value if the option was not specified.
*
* @param <T> The type of the option value.
* @param commandLine The {@link CommandLine} instance containing the parsed command line options.
* @param optionName The name of the option whose value is to be retrieved.
* @param converter A converter that converts the option's string value to its appropriate type.
* @return The value of the specified option converted to its type, or the default value if the
* option was not specified. Returns {@code null} if the option does not exist or if there is
* no default value and the option was not specified.
*/
public static <T> T getOptionValueOrDefault(
final CommandLine commandLine,
final String optionName,
final CommandLine.ITypeConverter<T> converter) {
return commandLine
.getParseResult()
.matchedOptionValue(optionName, getDefaultOptionValue(commandLine, optionName, converter));
}
/**
* Retrieves the default value for a specified command line option, converting it to its
* appropriate type.
*
* @param <T> The type of the option value.
* @param commandLine The {@link CommandLine} instance containing the parsed command line options.
* @param optionName The name of the option whose default value is to be retrieved.
* @param converter A converter that converts the option's default string value to its appropriate
* type.
* @return The default value of the specified option converted to its type, or {@code null} if the
* option does not exist, does not have a default value, or if an error occurs during
* conversion.
* @throws RuntimeException if there is an error converting the default value string to its type.
*/
private static <T> T getDefaultOptionValue(
final CommandLine commandLine,
final String optionName,
final CommandLine.ITypeConverter<T> converter) {
CommandLine.Model.OptionSpec optionSpec = commandLine.getCommandSpec().findOption(optionName);
if (optionSpec == null || commandLine.getDefaultValueProvider() == null) {
return null;
}
try {
String defaultValueString = commandLine.getDefaultValueProvider().defaultValue(optionSpec);
return defaultValueString != null
? converter.convert(defaultValueString)
: optionSpec.getValue();
} catch (Exception e) {
throw new RuntimeException(
"Failed to convert default value for option " + optionName + ": " + e.getMessage(), e);
}
}
}

@ -25,47 +25,37 @@ import com.google.common.annotations.VisibleForTesting;
import picocli.CommandLine;
import picocli.CommandLine.IDefaultValueProvider;
import picocli.CommandLine.IExecutionStrategy;
import picocli.CommandLine.IParameterExceptionHandler;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.ParseResult;
/** Custom Config option search and run handler. */
public class ConfigOptionSearchAndRunHandler extends CommandLine.RunLast {
public class ConfigDefaultValueProviderStrategy implements IExecutionStrategy {
private final IExecutionStrategy resultHandler;
private final IParameterExceptionHandler parameterExceptionHandler;
private final Map<String, String> environment;
/**
* Instantiates a new Config option search and run handler.
*
* @param resultHandler the result handler
* @param parameterExceptionHandler the parameter exception handler
* @param environment the environment variables map
*/
public ConfigOptionSearchAndRunHandler(
final IExecutionStrategy resultHandler,
final IParameterExceptionHandler parameterExceptionHandler,
final Map<String, String> environment) {
public ConfigDefaultValueProviderStrategy(
final IExecutionStrategy resultHandler, final Map<String, String> environment) {
this.resultHandler = resultHandler;
this.parameterExceptionHandler = parameterExceptionHandler;
this.environment = environment;
}
@Override
public List<Object> handle(final ParseResult parseResult) throws ParameterException {
public int execute(final ParseResult parseResult)
throws CommandLine.ExecutionException, ParameterException {
final CommandLine commandLine = parseResult.commandSpec().commandLine();
commandLine.setDefaultValueProvider(
createDefaultValueProvider(
commandLine,
new ConfigFileFinder().findConfiguration(environment, parseResult),
new ProfileFinder().findConfiguration(environment, parseResult)));
commandLine.setExecutionStrategy(resultHandler);
commandLine.setParameterExceptionHandler(parameterExceptionHandler);
commandLine.execute(parseResult.originalArgs().toArray(new String[0]));
return new ArrayList<>();
return commandLine.execute(parseResult.originalArgs().toArray(new String[0]));
}
/**
@ -73,10 +63,11 @@ public class ConfigOptionSearchAndRunHandler extends CommandLine.RunLast {
*
* @param commandLine the command line
* @param configFile the config file
* @param profile the profile file
* @return the default value provider
*/
@VisibleForTesting
IDefaultValueProvider createDefaultValueProvider(
public IDefaultValueProvider createDefaultValueProvider(
final CommandLine commandLine,
final Optional<File> configFile,
final Optional<InputStream> profile) {
@ -94,9 +85,4 @@ public class ConfigOptionSearchAndRunHandler extends CommandLine.RunLast {
p -> providers.add(TomlConfigurationDefaultProvider.fromInputStream(commandLine, p)));
return new CascadingDefaultProvider(providers);
}
@Override
public ConfigOptionSearchAndRunHandler self() {
return this;
}
}

@ -17,6 +17,7 @@ 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.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.plugin.BesuContext;
import org.hyperledger.besu.plugin.BesuPlugin;
import org.hyperledger.besu.plugin.services.BesuService;
@ -35,11 +36,13 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Predicate;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
@ -73,9 +76,13 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private Lifecycle state = Lifecycle.UNINITIALIZED;
private final Map<Class<?>, ? super BesuService> serviceRegistry = new HashMap<>();
private final List<BesuPlugin> plugins = new ArrayList<>();
private List<BesuPlugin> detectedPlugins = new ArrayList<>();
private List<String> requestedPlugins = new ArrayList<>();
private final List<BesuPlugin> registeredPlugins = new ArrayList<>();
private final List<String> pluginVersions = new ArrayList<>();
final List<String> lines = new ArrayList<>();
/**
* Add service.
@ -99,75 +106,96 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
return Optional.ofNullable((T) serviceRegistry.get(serviceType));
}
private List<BesuPlugin> detectPlugins(final PluginConfiguration config) {
ClassLoader pluginLoader =
pluginDirectoryLoader(config.getPluginsDir()).orElse(getClass().getClassLoader());
ServiceLoader<BesuPlugin> serviceLoader = ServiceLoader.load(BesuPlugin.class, pluginLoader);
return StreamSupport.stream(serviceLoader.spliterator(), false).toList();
}
/**
* Register plugins.
* Registers plugins based on the provided {@link PluginConfiguration}. This method finds plugins
* according to the configuration settings, filters them if necessary and then registers the
* filtered or found plugins
*
* @param pluginsDir the plugins dir
* @param config The configuration settings used to find and filter plugins for registration. The
* configuration includes the plugin directory and any configured plugin identifiers if
* applicable.
* @throws IllegalStateException if the system is not in the UNINITIALIZED state.
*/
public void registerPlugins(final Path pluginsDir) {
lines.add("Plugins:");
public void registerPlugins(final PluginConfiguration config) {
checkState(
state == Lifecycle.UNINITIALIZED,
"Besu plugins have already been registered. Cannot register additional plugins.");
"Besu plugins have already been registered. Cannot register additional plugins.");
state = Lifecycle.REGISTERING;
final ClassLoader pluginLoader =
pluginDirectoryLoader(pluginsDir).orElse(this.getClass().getClassLoader());
detectedPlugins = detectPlugins(config);
if (!config.getRequestedPlugins().isEmpty()) {
// Register only the plugins that were explicitly requested and validated
requestedPlugins = config.getRequestedPlugins();
state = Lifecycle.REGISTERING;
// Match and validate the requested plugins against the detected plugins
List<BesuPlugin> registeringPlugins =
matchAndValidateRequestedPlugins(requestedPlugins, detectedPlugins);
final ServiceLoader<BesuPlugin> serviceLoader =
ServiceLoader.load(BesuPlugin.class, pluginLoader);
registerPlugins(registeringPlugins);
} else {
// If no plugins were specified, register all detected plugins
registerPlugins(detectedPlugins);
}
}
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);
private List<BesuPlugin> matchAndValidateRequestedPlugins(
final List<String> requestedPluginNames, final List<BesuPlugin> detectedPlugins)
throws NoSuchElementException {
// Filter detected plugins to include only those that match the requested names
List<BesuPlugin> matchingPlugins =
detectedPlugins.stream()
.filter(plugin -> requestedPluginNames.contains(plugin.getClass().getSimpleName()))
.toList();
// Check if all requested plugins were found among the detected plugins
if (matchingPlugins.size() != requestedPluginNames.size()) {
// Find which requested plugins were not matched to throw a detailed exception
Set<String> matchedPluginNames =
matchingPlugins.stream()
.map(plugin -> plugin.getClass().getSimpleName())
.collect(Collectors.toSet());
String missingPlugins =
requestedPluginNames.stream()
.filter(name -> !matchedPluginNames.contains(name))
.collect(Collectors.joining(", "));
throw new NoSuchElementException(
"The following requested plugins were not found: " + missingPlugins);
}
return matchingPlugins;
}
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()));
private void registerPlugins(final List<BesuPlugin> pluginsToRegister) {
for (final BesuPlugin plugin : pluginsToRegister) {
if (registerPlugin(plugin)) {
registeredPlugins.add(plugin);
}
}
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;
private boolean registerPlugin(final BesuPlugin plugin) {
try {
plugin.register(this);
LOG.info("Registered plugin of type {}.", plugin.getClass().getName());
pluginVersions.add(plugin.getVersion());
} catch (final Exception e) {
LOG.error(
"Error registering plugin of type "
+ plugin.getClass().getName()
+ ", start and stop will not be called.",
e);
return false;
}
return true;
}
/** Before external services. */
@ -178,7 +206,7 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
Lifecycle.REGISTERED,
state);
state = Lifecycle.BEFORE_EXTERNAL_SERVICES_STARTED;
final Iterator<BesuPlugin> pluginsIterator = plugins.iterator();
final Iterator<BesuPlugin> pluginsIterator = registeredPlugins.iterator();
while (pluginsIterator.hasNext()) {
final BesuPlugin plugin = pluginsIterator.next();
@ -209,7 +237,7 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED,
state);
state = Lifecycle.BEFORE_MAIN_LOOP_STARTED;
final Iterator<BesuPlugin> pluginsIterator = plugins.iterator();
final Iterator<BesuPlugin> pluginsIterator = registeredPlugins.iterator();
while (pluginsIterator.hasNext()) {
final BesuPlugin plugin = pluginsIterator.next();
@ -240,7 +268,7 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
state);
state = Lifecycle.STOPPING;
for (final BesuPlugin plugin : plugins) {
for (final BesuPlugin plugin : registeredPlugins) {
try {
plugin.stop();
LOG.debug("Stopped plugin of type {}.", plugin.getClass().getName());
@ -253,11 +281,6 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
state = Lifecycle.STOPPED;
}
@Override
public Collection<String> getPluginVersions() {
return Collections.unmodifiableList(pluginVersions);
}
private static URL pathToURIOrNull(final Path p) {
try {
return p.toUri().toURL();
@ -266,16 +289,6 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
}
}
/**
* 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());
@ -299,14 +312,73 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
return Optional.empty();
}
@Override
public Collection<String> getPluginVersions() {
return Collections.unmodifiableList(pluginVersions);
}
/**
* Gets plugins.
*
* @return the plugins
*/
@VisibleForTesting
List<BesuPlugin> getRegisteredPlugins() {
return Collections.unmodifiableList(registeredPlugins);
}
/**
* Gets named plugins.
*
* @return the named plugins
*/
public Map<String, BesuPlugin> getNamedPlugins() {
return plugins.stream()
return registeredPlugins.stream()
.filter(plugin -> plugin.getName().isPresent())
.collect(Collectors.toMap(plugin -> plugin.getName().get(), plugin -> plugin, (a, b) -> b));
}
/**
* Generates a summary log of plugin registration. The summary includes registered plugins,
* detected but not registered (skipped) plugins
*
* @return A list of strings, each representing a line in the summary log.
*/
public List<String> getPluginsSummaryLog() {
List<String> summary = new ArrayList<>();
summary.add("Plugin Registration Summary:");
// Log registered plugins with their names and versions
if (registeredPlugins.isEmpty()) {
summary.add("No plugins have been registered.");
} else {
summary.add("Registered Plugins:");
registeredPlugins.forEach(
plugin ->
summary.add(
String.format(
" - %s (Version: %s)",
plugin.getClass().getSimpleName(), plugin.getVersion())));
}
// Identify and log detected but not registered (skipped) plugins
List<String> skippedPlugins =
detectedPlugins.stream()
.filter(plugin -> !registeredPlugins.contains(plugin))
.map(plugin -> plugin.getClass().getSimpleName())
.toList();
if (!skippedPlugins.isEmpty()) {
summary.add("Skipped Plugins:");
skippedPlugins.forEach(
pluginName ->
summary.add(String.format(" - %s (Detected but not registered)", pluginName)));
}
summary.add(
String.format(
"TOTAL = %d of %d plugins successfully registered.",
registeredPlugins.size(), detectedPlugins.size()));
return summary;
}
}

@ -0,0 +1,110 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.cli;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.cli.util.CommandLineUtils.getOptionValueOrDefault;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.cli.util.CommandLineUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
/**
* Unit tests for {@link CommandLineUtils} focusing on the retrieval of option values
* (getOptionValueOrDefault).
*/
public class CommandLineUtilsDefaultsTest {
private static final String OPTION_NAME = "option";
private static final String OPTION_VALUE = "optionValue";
private static final String DEFAULT_VALUE = "defaultValue";
public final CommandLine.ITypeConverter<String> converter = String::valueOf;
private CommandLine commandLine;
private CommandLine.Model.OptionSpec optionSpec;
private CommandLine.IDefaultValueProvider defaultValueProvider;
private CommandLine.ParseResult parseResult;
@BeforeEach
public void setUp() {
commandLine = mock(CommandLine.class);
parseResult = mock(CommandLine.ParseResult.class);
CommandLine.Model.CommandSpec commandSpec = mock(CommandLine.Model.CommandSpec.class);
optionSpec = mock(CommandLine.Model.OptionSpec.class);
defaultValueProvider = mock(CommandLine.IDefaultValueProvider.class);
when(commandLine.getParseResult()).thenReturn(parseResult);
when(commandLine.getCommandSpec()).thenReturn(commandSpec);
when(commandLine.getDefaultValueProvider()).thenReturn(defaultValueProvider);
when(parseResult.matchedOptionValue(anyString(), any())).thenCallRealMethod();
when(commandSpec.findOption(OPTION_NAME)).thenReturn(optionSpec);
}
@Test
public void testGetOptionValueOrDefault_UserProvidedValue() {
when(parseResult.matchedOption(OPTION_NAME)).thenReturn(optionSpec);
when(optionSpec.getValue()).thenReturn(OPTION_VALUE);
String result = getOptionValueOrDefault(commandLine, OPTION_NAME, converter);
assertThat(result).isEqualTo(OPTION_VALUE);
}
@Test
public void testGetOptionValueOrDefault_DefaultValue() throws Exception {
when(defaultValueProvider.defaultValue(optionSpec)).thenReturn(DEFAULT_VALUE);
String result = getOptionValueOrDefault(commandLine, OPTION_NAME, converter);
assertThat(result).isEqualTo(DEFAULT_VALUE);
}
@Test
public void userOptionOverridesDefaultValue() throws Exception {
when(parseResult.matchedOption(OPTION_NAME)).thenReturn(optionSpec);
when(optionSpec.getValue()).thenReturn(OPTION_VALUE);
when(defaultValueProvider.defaultValue(optionSpec)).thenReturn(DEFAULT_VALUE);
String result = getOptionValueOrDefault(commandLine, OPTION_NAME, converter);
assertThat(result).isEqualTo(OPTION_VALUE);
}
@Test
public void testGetOptionValueOrDefault_NoValueOrDefault() {
String result = getOptionValueOrDefault(commandLine, OPTION_NAME, converter);
assertThat(result).isNull();
}
@Test
public void testGetOptionValueOrDefault_ConversionFailure() throws Exception {
when(defaultValueProvider.defaultValue(optionSpec)).thenReturn(DEFAULT_VALUE);
CommandLine.ITypeConverter<Integer> failingConverter =
value -> {
throw new Exception("Conversion failed");
};
String actualMessage =
assertThrows(
RuntimeException.class,
() -> getOptionValueOrDefault(commandLine, OPTION_NAME, failingConverter))
.getMessage();
final String expectedMessage =
"Failed to convert default value for option option: Conversion failed";
assertThat(actualMessage).isEqualTo(expectedMessage);
}
}

@ -69,7 +69,6 @@ public class CommandLineUtilsTest {
commandLine.setDefaultValueProvider(new EnvironmentVariableDefaultProvider(environment));
}
// Completely disables p2p within Besu.
@Option(
names = {"--option-enabled"},
arity = "1")

@ -52,7 +52,7 @@ import picocli.CommandLine.ParseResult;
import picocli.CommandLine.RunLast;
@ExtendWith(MockitoExtension.class)
public class ConfigOptionSearchAndRunHandlerTest {
public class ConfigDefaultValueProviderStrategyTest {
private static final String CONFIG_FILE_OPTION_NAME = "--config-file";
@TempDir public Path temp;
@ -61,7 +61,7 @@ public class ConfigOptionSearchAndRunHandlerTest {
private final IExecutionStrategy resultHandler = new RunLast();
private final Map<String, String> environment = singletonMap("BESU_LOGGING", "ERROR");
private ConfigOptionSearchAndRunHandler configParsingHandler;
private ConfigDefaultValueProviderStrategy configParsingHandler;
@Mock ParseResult mockParseResult;
@Mock CommandSpec mockCommandSpec;
@ -84,60 +84,52 @@ public class ConfigOptionSearchAndRunHandlerTest {
lenient().when(mockConfigOptionSpec.getter()).thenReturn(mockConfigOptionGetter);
levelOption = new LoggingLevelOption();
levelOption.setLogLevel("INFO");
configParsingHandler =
new ConfigOptionSearchAndRunHandler(
resultHandler, mockParameterExceptionHandler, environment);
configParsingHandler = new ConfigDefaultValueProviderStrategy(resultHandler, environment);
}
@Test
public void handleWithCommandLineOption() throws Exception {
when(mockConfigOptionGetter.get()).thenReturn(Files.createTempFile("tmp", "txt").toFile());
final List<Object> result = configParsingHandler.handle(mockParseResult);
configParsingHandler.execute(mockParseResult);
verify(mockCommandLine).setDefaultValueProvider(any(IDefaultValueProvider.class));
verify(mockCommandLine).setExecutionStrategy(eq(resultHandler));
verify(mockCommandLine).setParameterExceptionHandler(eq(mockParameterExceptionHandler));
verify(mockCommandLine).execute(anyString());
assertThat(result).isEmpty();
}
@Test
public void handleWithEnvironmentVariable() throws IOException {
when(mockParseResult.hasMatchedOption(CONFIG_FILE_OPTION_NAME)).thenReturn(false);
final ConfigOptionSearchAndRunHandler environmentConfigFileParsingHandler =
new ConfigOptionSearchAndRunHandler(
final ConfigDefaultValueProviderStrategy environmentConfigFileParsingHandler =
new ConfigDefaultValueProviderStrategy(
resultHandler,
mockParameterExceptionHandler,
singletonMap(
"BESU_CONFIG_FILE",
Files.createFile(temp.resolve("tmp")).toFile().getAbsolutePath()));
when(mockParseResult.hasMatchedOption(CONFIG_FILE_OPTION_NAME)).thenReturn(false);
environmentConfigFileParsingHandler.handle(mockParseResult);
environmentConfigFileParsingHandler.execute(mockParseResult);
}
@Test
public void handleWithCommandLineOptionShouldRaiseExceptionIfNoFileParam() throws Exception {
final String error_message = "an error occurred during get";
when(mockConfigOptionGetter.get()).thenThrow(new Exception(error_message));
assertThatThrownBy(() -> configParsingHandler.handle(mockParseResult))
assertThatThrownBy(() -> configParsingHandler.execute(mockParseResult))
.isInstanceOf(Exception.class)
.hasMessage(error_message);
}
@Test
public void handleWithEnvironmentVariableOptionShouldRaiseExceptionIfNoFileParam() {
final ConfigOptionSearchAndRunHandler environmentConfigFileParsingHandler =
new ConfigOptionSearchAndRunHandler(
resultHandler,
mockParameterExceptionHandler,
singletonMap("BESU_CONFIG_FILE", "not_found.toml"));
final ConfigDefaultValueProviderStrategy environmentConfigFileParsingHandler =
new ConfigDefaultValueProviderStrategy(
resultHandler, singletonMap("BESU_CONFIG_FILE", "not_found.toml"));
when(mockParseResult.hasMatchedOption(CONFIG_FILE_OPTION_NAME)).thenReturn(false);
assertThatThrownBy(() -> environmentConfigFileParsingHandler.handle(mockParseResult))
assertThatThrownBy(() -> environmentConfigFileParsingHandler.execute(mockParseResult))
.isInstanceOf(CommandLine.ParameterException.class);
}
@ -163,15 +155,14 @@ public class ConfigOptionSearchAndRunHandlerTest {
public void handleThrowsErrorWithWithEnvironmentVariableAndCommandLineSpecified()
throws IOException {
final ConfigOptionSearchAndRunHandler environmentConfigFileParsingHandler =
new ConfigOptionSearchAndRunHandler(
final ConfigDefaultValueProviderStrategy environmentConfigFileParsingHandler =
new ConfigDefaultValueProviderStrategy(
resultHandler,
mockParameterExceptionHandler,
singletonMap("BESU_CONFIG_FILE", temp.resolve("tmp").toFile().getAbsolutePath()));
when(mockParseResult.hasMatchedOption(CONFIG_FILE_OPTION_NAME)).thenReturn(true);
assertThatThrownBy(() -> environmentConfigFileParsingHandler.handle(mockParseResult))
assertThatThrownBy(() -> environmentConfigFileParsingHandler.execute(mockParseResult))
.isInstanceOf(CommandLine.ParameterException.class);
}
}

@ -0,0 +1,92 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.ethereum.core.plugins;
import static java.util.Objects.requireNonNull;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
/**
* Configuration for managing plugins, including their information, detection type, and directory.
*/
public class PluginConfiguration {
private final List<PluginInfo> requestedPlugins;
private final Path pluginsDir;
/**
* Constructs a new PluginConfiguration with the specified plugin information and requestedPlugins
* directory.
*
* @param requestedPlugins List of {@link PluginInfo} objects representing the requestedPlugins.
* @param pluginsDir The directory where requestedPlugins are located.
*/
public PluginConfiguration(final List<PluginInfo> requestedPlugins, final Path pluginsDir) {
this.requestedPlugins = requestedPlugins;
this.pluginsDir = pluginsDir;
}
/**
* Constructs a PluginConfiguration with specified plugins using the default directory.
*
* @param requestedPlugins List of plugins for consideration or registration. discoverable plugins
* are.
*/
public PluginConfiguration(final List<PluginInfo> requestedPlugins) {
this.requestedPlugins = requestedPlugins;
this.pluginsDir = PluginConfiguration.defaultPluginsDir();
}
/**
* Constructs a PluginConfiguration with the specified plugins directory
*
* @param pluginsDir The directory where plugins are located. Cannot be null.
*/
public PluginConfiguration(final Path pluginsDir) {
this.requestedPlugins = null;
this.pluginsDir = requireNonNull(pluginsDir);
}
/**
* Returns the names of requested plugins, or an empty list if none.
*
* @return List of requested plugin names, never {@code null}.
*/
public List<String> getRequestedPlugins() {
return requestedPlugins == null
? Collections.emptyList()
: requestedPlugins.stream().map(PluginInfo::name).toList();
}
public Path getPluginsDir() {
return pluginsDir;
}
/**
* Returns the default plugins directory based on system properties.
*
* @return The default {@link Path} to the plugin's directory.
*/
public static Path defaultPluginsDir() {
final String pluginsDirProperty = System.getProperty("besu.plugins.dir");
if (pluginsDirProperty == null) {
return Paths.get(System.getProperty("besu.home", "."), "plugins");
} else {
return Paths.get(pluginsDirProperty);
}
}
}

@ -0,0 +1,37 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.ethereum.core.plugins;
/** Represents information about a plugin, including its name. */
public final class PluginInfo {
private final String name;
/**
* Constructs a new PluginInfo instance with the specified name.
*
* @param name The name of the plugin. Cannot be null or empty.
* @throws IllegalArgumentException if the name is null or empty.
*/
public PluginInfo(final String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Plugin name cannot be null or empty.");
}
this.name = name;
}
public String name() {
return name;
}
}

@ -69,7 +69,7 @@ Calculated : ${currentHash}
tasks.register('checkAPIChanges', FileStateChecker) {
description = "Checks that the API for the Plugin-API project does not change without deliberate thought"
files = sourceSets.main.allJava.files
knownHash = '0mJiCGsToqx5aAJEvwnT3V0R8o4PXBYWiB0wT6CMpuo='
knownHash = '78xbZ20PDB9CDcaSVY92VA8cXWGu4GwaZkvegWgep24='
}
check.dependsOn('checkAPIChanges')

@ -84,4 +84,24 @@ public interface BesuPlugin {
* started.
*/
void stop();
/**
* Retrieves the version information of the plugin. It constructs a version string using the
* implementation title and version from the package information. If either the title or version
* is not available, it defaults to the class's simple name and "Unknown Version", respectively.
*
* @return A string representing the plugin's version information, formatted as "Title/vVersion".
*/
default String getVersion() {
Package pluginPackage = this.getClass().getPackage();
String implTitle =
Optional.ofNullable(pluginPackage.getImplementationTitle())
.filter(title -> !title.isBlank())
.orElseGet(() -> this.getClass().getSimpleName());
String implVersion =
Optional.ofNullable(pluginPackage.getImplementationVersion())
.filter(version -> !version.isBlank())
.orElse("<Unknown Version>");
return implTitle + "/v" + implVersion;
}
}

Loading…
Cancel
Save