1818import static com .google .common .base .Preconditions .checkState ;
1919
2020import org .hyperledger .besu .ethereum .core .plugins .PluginConfiguration ;
21- import org .hyperledger .besu .ethereum .core .plugins .PluginsVerificationMode ;
2221import org .hyperledger .besu .plugin .BesuPlugin ;
2322import org .hyperledger .besu .plugin .ServiceManager ;
2423import org .hyperledger .besu .plugin .services .BesuService ;
2524import org .hyperledger .besu .plugin .services .PluginVersionsProvider ;
26- import org .hyperledger .besu .util .BesuVersionUtils ;
2725
2826import java .io .IOException ;
2927import java .net .MalformedURLException ;
30- import java .net .URI ;
31- import java .net .URISyntaxException ;
3228import java .net .URL ;
3329import java .net .URLClassLoader ;
3430import java .nio .file .Files ;
3531import java .nio .file .Path ;
3632import java .util .ArrayList ;
3733import java .util .Collections ;
3834import java .util .HashMap ;
39- import java .util .HashSet ;
4035import java .util .Iterator ;
4136import java .util .LinkedHashMap ;
4237import java .util .List ;
4338import java .util .Map ;
4439import java .util .NoSuchElementException ;
45- import java .util .Objects ;
4640import java .util .Optional ;
4741import java .util .ServiceLoader ;
4842import java .util .Set ;
4943import java .util .stream .Collectors ;
5044import java .util .stream .Stream ;
5145import java .util .stream .StreamSupport ;
5246
53- import com .fasterxml .jackson .core .type .TypeReference ;
54- import com .fasterxml .jackson .databind .ObjectMapper ;
5547import com .google .common .annotations .VisibleForTesting ;
56- import org .apache .commons .lang3 .StringUtils ;
5748import org .slf4j .Logger ;
5849import org .slf4j .LoggerFactory ;
59- import org .slf4j .spi .LoggingEventBuilder ;
6050
6151/** The Besu plugin context implementation. */
6252public class BesuPluginContextImpl implements ServiceManager , PluginVersionsProvider {
@@ -96,6 +86,7 @@ private enum Lifecycle {
9686
9787 private final Map <String , String > pluginVersions = new LinkedHashMap <>();
9888 private PluginConfiguration config ;
89+ private URLClassLoader pluginClassLoader ;
9990
10091 /** Instantiates a new Besu plugin context. */
10192 public BesuPluginContextImpl () {}
@@ -345,6 +336,16 @@ public void stopPlugins() {
345336 }
346337 }
347338
339+ // Close the plugin classloader to release file handles
340+ if (pluginClassLoader != null ) {
341+ try {
342+ pluginClassLoader .close ();
343+ LOG .debug ("Closed plugin classloader" );
344+ } catch (final IOException e ) {
345+ LOG .debug ("Error closing plugin classloader" , e );
346+ }
347+ }
348+
348349 LOG .debug ("Plugin shutdown complete." );
349350 state = Lifecycle .STOPPED ;
350351 }
@@ -368,11 +369,16 @@ private List<BesuPlugin> detectPlugins(final PluginConfiguration config) {
368369 .filter (p -> p .getFileName ().toString ().endsWith (".jar" ))
369370 .map (BesuPluginContextImpl ::pathToURIOrNull )
370371 .toArray (URL []::new );
371- final var pluginClassLoader = new URLClassLoader (pluginJarURLs );
372+ // The URLClassLoader must remain open for the entire application lifecycle
373+ // as plugins may load classes lazily during their operation. The classloader
374+ // will be closed in stopPlugins() during shutdown.
375+ this .pluginClassLoader =
376+ new URLClassLoader (pluginJarURLs , this .getClass ().getClassLoader ());
372377 ServiceLoader <BesuPlugin > serviceLoader =
373- ServiceLoader .load (BesuPlugin .class , pluginClassLoader );
378+ ServiceLoader .load (BesuPlugin .class , this . pluginClassLoader );
374379 final var foundPlugins = StreamSupport .stream (serviceLoader .spliterator (), false ).toList ();
375- verifyPlugins (config .getPluginsVerificationMode (), pluginClassLoader , foundPlugins );
380+ PluginVerifier .verify (
381+ config .getPluginsVerificationMode (), this .pluginClassLoader , foundPlugins );
376382
377383 return foundPlugins ;
378384 } catch (final MalformedURLException e ) {
@@ -387,222 +393,6 @@ private List<BesuPlugin> detectPlugins(final PluginConfiguration config) {
387393 return List .of ();
388394 }
389395
390- private void verifyPlugins (
391- final PluginsVerificationMode pluginsVerificationMode ,
392- final URLClassLoader pluginClassLoader ,
393- final List <BesuPlugin > plugins )
394- throws IOException {
395-
396- verifyNamesAreUnique (plugins );
397-
398- final var pluginsArtifactData = getPluginsArtifactData (pluginClassLoader , plugins );
399- LOG .debug ("Loaded pluginsArtifactData: {}" , pluginsArtifactData );
400-
401- final var catalogLessArtifacts =
402- pluginsArtifactData .stream ()
403- .filter (ad -> ad .catalog ().equals (ArtifactCatalog .NO_CATALOG ))
404- .toList ();
405-
406- if (!catalogLessArtifacts .isEmpty ()) {
407- final LoggingEventBuilder leb =
408- pluginsVerificationMode .failOnCatalogLess () ? LOG .atError () : LOG .atWarn ();
409- catalogLessArtifacts .forEach (
410- ad ->
411- leb .log (
412- "Artifact {} containing plugins {} is without a catalog" ,
413- ad .name (),
414- ad .plugins ().stream ().map (BesuPlugin ::getName ).toList ()));
415-
416- if (pluginsVerificationMode .failOnCatalogLess ()) {
417- throw new IllegalStateException (
418- "Plugins verification failed, see previous logs for more details." );
419- }
420- }
421-
422- verifyCatalogs (pluginClassLoader , pluginsVerificationMode , pluginsArtifactData );
423- }
424-
425- private void verifyNamesAreUnique (final List <BesuPlugin > loadedPlugins ) {
426- final var pluginNameCounts =
427- loadedPlugins .stream ()
428- .collect (Collectors .groupingBy (BesuPlugin ::getName , Collectors .counting ()));
429-
430- final var duplicateNames =
431- pluginNameCounts .entrySet ().stream ()
432- .filter (entry -> entry .getValue () > 1 )
433- .map (Map .Entry ::getKey )
434- .toList ();
435-
436- if (!duplicateNames .isEmpty ()) {
437- throw new IllegalStateException (
438- "Plugins with same name detected: " + String .join (", " , duplicateNames ));
439- }
440- }
441-
442- private List <ArtifactInfo > getPluginsArtifactData (
443- final URLClassLoader pluginClassLoader , final List <BesuPlugin > plugins ) throws IOException {
444- final var pluginsByArtifact =
445- plugins .stream ()
446- .collect (
447- Collectors .groupingBy (
448- plugin -> {
449- try {
450- return plugin
451- .getClass ()
452- .getProtectionDomain ()
453- .getCodeSource ()
454- .getLocation ()
455- .toURI ()
456- .getSchemeSpecificPart ();
457- } catch (URISyntaxException e ) {
458- throw new RuntimeException (
459- "Error getting artifact URL for plugin " + plugin .getName (), e );
460- }
461- }));
462-
463- final var allCatalogs =
464- Collections .list (pluginClassLoader .getResources ("META-INF/plugin-artifacts-catalog.json" ));
465-
466- final var artifactsWithCatalogs =
467- allCatalogs .stream ()
468- .collect (
469- Collectors .toMap (
470- url -> {
471- try {
472- return StringUtils .substringBefore (new URI (url .getPath ()).getPath (), '!' );
473- } catch (URISyntaxException e ) {
474- throw new RuntimeException (e );
475- }
476- },
477- ArtifactCatalog ::load ));
478-
479- return pluginsByArtifact .entrySet ().stream ()
480- .map (
481- entry ->
482- new ArtifactInfo (
483- entry .getKey (),
484- entry .getValue (),
485- artifactsWithCatalogs .getOrDefault (entry .getKey (), ArtifactCatalog .NO_CATALOG )))
486- .toList ();
487- }
488-
489- private void verifyCatalogs (
490- final URLClassLoader pluginClassLoader ,
491- final PluginsVerificationMode pluginsVerificationMode ,
492- final List <ArtifactInfo > pluginsArtifactData )
493- throws IOException {
494- verifyBesuVersions (pluginsVerificationMode , pluginsArtifactData );
495- verifyBesuPluginsDependencyConflicts (
496- pluginClassLoader , pluginsVerificationMode , pluginsArtifactData );
497- verifyPluginsDependencyConflicts (pluginsVerificationMode , pluginsArtifactData );
498- }
499-
500- private void verifyBesuVersions (
501- final PluginsVerificationMode pluginsVerificationMode ,
502- final List <ArtifactInfo > pluginsArtifactData ) {
503- final var besuRunningVersion = BesuVersionUtils .shortVersion ();
504- final var besuVersionConflictFound =
505- pluginsArtifactData .stream ()
506- .anyMatch (
507- artifactInfo -> {
508- final var pluginBuildBesuVersion = artifactInfo .catalog ().besuVersion ();
509- if (!besuRunningVersion .equals (pluginBuildBesuVersion )) {
510- final LoggingEventBuilder leb =
511- pluginsVerificationMode .failOnBesuVersionConflict ()
512- ? LOG .atError ()
513- : LOG .atWarn ();
514- leb .log (
515- "Plugin artifact {} is built against Besu version {} while current running Besu version is {}" ,
516- artifactInfo .name (),
517- pluginBuildBesuVersion == null ? "unknown" : pluginBuildBesuVersion ,
518- besuRunningVersion );
519- return true ;
520- }
521- return false ;
522- });
523-
524- if (besuVersionConflictFound && pluginsVerificationMode .failOnBesuVersionConflict ()) {
525- throw new IllegalStateException (
526- "Plugins verification failed, see previous logs for more details." );
527- }
528- }
529-
530- private void verifyPluginsDependencyConflicts (
531- final PluginsVerificationMode pluginsVerificationMode ,
532- final List <ArtifactInfo > pluginsArtifactInfo ) {
533- final var alreadyChecked = HashSet .<ArtifactInfo >newHashSet (pluginsArtifactInfo .size ());
534-
535- boolean verificationFailed = false ;
536-
537- for (final var artifactInfo : pluginsArtifactInfo ) {
538- alreadyChecked .add (artifactInfo );
539- for (final var dep : artifactInfo .catalog ().dependencies ()) {
540- for (final var otherArtifactInfo : pluginsArtifactInfo ) {
541- if (!alreadyChecked .contains (otherArtifactInfo )) {
542- final var otherCatalog = otherArtifactInfo .catalog ();
543- final var maybeOtherDep = otherCatalog .getByNameAndGroup (dep );
544- if (maybeOtherDep .isPresent ()) {
545- final var otherDep = maybeOtherDep .get ();
546- if (!dep .version ().equals (otherDep .version ())) {
547- verificationFailed = pluginsVerificationMode .failOnPluginDependencyConflict ();
548- final LoggingEventBuilder leb = verificationFailed ? LOG .atError () : LOG .atWarn ();
549- leb .log (
550- "Plugin artifacts {} and {} bring the same dependency {}:{} but with different versions: {} and {}" ,
551- artifactInfo .name (),
552- otherArtifactInfo .name (),
553- dep .group (),
554- dep .name (),
555- dep .version (),
556- otherDep .version ());
557- }
558- }
559- }
560- }
561- }
562- }
563-
564- if (verificationFailed ) {
565- throw new IllegalStateException (
566- "Plugins verification failed, see previous logs for more details." );
567- }
568- }
569-
570- private void verifyBesuPluginsDependencyConflicts (
571- final URLClassLoader pluginClassLoader ,
572- final PluginsVerificationMode pluginsVerificationMode ,
573- final List <ArtifactInfo > pluginsArtifactData )
574- throws IOException {
575- final var besuArtifactsCatalog =
576- Collections .list (pluginClassLoader .getResources ("META-INF/besu-artifacts-catalog.json" ));
577-
578- final var besuDependencies = ArtifactDependency .loadList (besuArtifactsCatalog .get (0 ));
579-
580- besuDependencies .forEach (
581- besuDependency -> {
582- final var conflicts =
583- pluginsArtifactData .stream ()
584- .filter (pad -> pad .catalog ().containsByNameAndGroup (besuDependency ))
585- .toList ();
586- if (!conflicts .isEmpty ()) {
587- final LoggingEventBuilder leb =
588- pluginsVerificationMode .failOnBesuDependencyConflict ()
589- ? LOG .atError ()
590- : LOG .atWarn ();
591- conflicts .forEach (
592- ai -> {
593- leb .log (
594- "Plugin artifact {} brings the dependency {} that is already provided by Besu" ,
595- ai .name (),
596- besuDependency .group () + ":" + besuDependency .name ());
597- });
598- if (pluginsVerificationMode .failOnBesuDependencyConflict ()) {
599- throw new IllegalStateException (
600- "Plugins verification failed, see previous logs for more details." );
601- }
602- }
603- });
604- }
605-
606396 @ Override
607397 public Map <String , String > getPluginVersions () {
608398 return Collections .unmodifiableMap (pluginVersions );
@@ -669,56 +459,4 @@ public List<String> getPluginsSummaryLog() {
669459
670460 return summary ;
671461 }
672-
673- record ArtifactInfo (String name , List <BesuPlugin > plugins , ArtifactCatalog catalog ) {
674- @ Override
675- public String toString () {
676- return "ArtifactInfo{"
677- + "name='"
678- + name
679- + '\''
680- + ", plugins="
681- + plugins .stream ().map (BesuPlugin ::getName ).collect (Collectors .joining (", " ))
682- + ", catalog="
683- + catalog
684- + '}' ;
685- }
686- }
687-
688- record ArtifactCatalog (String besuVersion , List <ArtifactDependency > dependencies ) {
689- static final ArtifactCatalog NO_CATALOG = new ArtifactCatalog (null , List .of ());
690-
691- static ArtifactCatalog load (final URL url ) {
692- final var objectMapper = new ObjectMapper ();
693- try {
694- return objectMapper .readValue (url , ArtifactCatalog .class );
695- } catch (IOException e ) {
696- throw new RuntimeException (e );
697- }
698- }
699-
700- public boolean containsByNameAndGroup (final ArtifactDependency other ) {
701- return dependencies .stream ().anyMatch (other ::equalsByNameAndGroup );
702- }
703-
704- public Optional <ArtifactDependency > getByNameAndGroup (final ArtifactDependency other ) {
705- return dependencies .stream ().filter (other ::equalsByNameAndGroup ).findFirst ();
706- }
707- }
708-
709- record ArtifactDependency (
710- String name , String group , String version , String classifier , String filename ) {
711- boolean equalsByNameAndGroup (final ArtifactDependency other ) {
712- return Objects .equals (name , other .name ) && Objects .equals (group , other .group );
713- }
714-
715- static List <ArtifactDependency > loadList (final URL url ) {
716- final var objectMapper = new ObjectMapper ();
717- try {
718- return objectMapper .readValue (url , new TypeReference <>() {});
719- } catch (IOException e ) {
720- throw new RuntimeException (e );
721- }
722- }
723- }
724462}
0 commit comments