mirror of
https://github.com/vacp2p/linea-besu.git
synced 2026-01-09 15:37:54 -05:00
Enhanced control over plugins registration (#6700)
Signed-off-by: Gabriel-Trintinalia <gabriel.trintinalia@consensys.net>
This commit is contained in:
committed by
GitHub
parent
61432831d5
commit
a1f73d925e
@@ -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.");
|
||||
|
||||
final ClassLoader pluginLoader =
|
||||
pluginDirectoryLoader(pluginsDir).orElse(this.getClass().getClassLoader());
|
||||
|
||||
"Besu plugins have already been registered. Cannot register additional plugins.");
|
||||
state = Lifecycle.REGISTERING;
|
||||
|
||||
final ServiceLoader<BesuPlugin> serviceLoader =
|
||||
ServiceLoader.load(BesuPlugin.class, pluginLoader);
|
||||
detectedPlugins = detectPlugins(config);
|
||||
if (!config.getRequestedPlugins().isEmpty()) {
|
||||
// Register only the plugins that were explicitly requested and validated
|
||||
requestedPlugins = config.getRequestedPlugins();
|
||||
|
||||
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);
|
||||
// Match and validate the requested plugins against the detected plugins
|
||||
List<BesuPlugin> registeringPlugins =
|
||||
matchAndValidateRequestedPlugins(requestedPlugins, detectedPlugins);
|
||||
|
||||
registerPlugins(registeringPlugins);
|
||||
} else {
|
||||
// If no plugins were specified, register all detected plugins
|
||||
registerPlugins(detectedPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user