From 6a039de8db3a9dc78fe884e5d150db24df1933bb Mon Sep 17 00:00:00 2001 From: md_5 Date: Fri, 9 Apr 2021 13:13:55 +1000 Subject: [PATCH] Add preview of automatic library support Example plugin.yml usage: ``` libraries: - com.squareup.okhttp3:okhttp:4.9.0 ``` Libraries will only be accessible to plugins and their transitive depends, allowing for multiple versions of the same library to be used by different plugins. This is a preview feature. Feedback is welcome so that it may be refined before being made widely available. --- api/pom.xml | 21 +++ .../md_5/bungee/api/plugin/LibraryLoader.java | 123 ++++++++++++++++++ .../bungee/api/plugin/PluginClassloader.java | 96 +++++++++++++- .../bungee/api/plugin/PluginDescription.java | 6 + .../md_5/bungee/api/plugin/PluginManager.java | 40 +++++- proxy/pom.xml | 25 ++++ 6 files changed, 300 insertions(+), 11 deletions(-) create mode 100644 api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java diff --git a/api/pom.xml b/api/pom.xml index a502e8c3..6e8e84f5 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -49,6 +49,27 @@ ${netty.version} compile + + org.apache.maven + maven-resolver-provider + 3.8.1 + + provided + + + org.apache.maven.resolver + maven-resolver-connector-basic + 1.6.2 + + provided + + + org.apache.maven.resolver + maven-resolver-transport-http + 1.6.2 + + provided + org.yaml snakeyaml diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java b/api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java new file mode 100644 index 00000000..48c31996 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java @@ -0,0 +1,123 @@ +package net.md_5.bungee.api.plugin; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResolutionException; +import org.eclipse.aether.resolution.DependencyResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.AbstractTransferListener; +import org.eclipse.aether.transfer.TransferCancelledException; +import org.eclipse.aether.transfer.TransferEvent; +import org.eclipse.aether.transport.http.HttpTransporterFactory; + +class LibraryLoader +{ + + private final Logger logger; + private final RepositorySystem repository; + private final DefaultRepositorySystemSession session; + private final List repositories; + + public LibraryLoader(Logger logger) + { + this.logger = logger; + + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService( RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class ); + locator.addService( TransporterFactory.class, HttpTransporterFactory.class ); + + this.repository = locator.getService( RepositorySystem.class ); + this.session = MavenRepositorySystemUtils.newSession(); + + session.setChecksumPolicy( RepositoryPolicy.CHECKSUM_POLICY_FAIL ); + session.setLocalRepositoryManager( repository.newLocalRepositoryManager( session, new LocalRepository( "libraries" ) ) ); + session.setTransferListener( new AbstractTransferListener() + { + @Override + public void transferStarted(TransferEvent event) throws TransferCancelledException + { + logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() ); + } + } ); + session.setReadOnly(); + + this.repositories = repository.newResolutionRepositories( session, Arrays.asList( new RemoteRepository.Builder( "central", "default", "https://repo.maven.apache.org/maven2" ).build() ) ); + } + + public ClassLoader createLoader(PluginDescription desc) + { + if ( desc.getLibraries().isEmpty() ) + { + return null; + } + logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[] + { + desc.getName(), desc.getLibraries().size() + } ); + + List dependencies = new ArrayList<>(); + for ( String library : desc.getLibraries() ) + { + Artifact artifact = new DefaultArtifact( library ); + Dependency dependency = new Dependency( artifact, null ); + + dependencies.add( dependency ); + } + + DependencyResult result; + try + { + result = repository.resolveDependencies( session, new DependencyRequest( new CollectRequest( (Dependency) null, dependencies, repositories ), null ) ); + } catch ( DependencyResolutionException ex ) + { + throw new RuntimeException( "Error resolving libraries", ex ); + } + + List jarFiles = new ArrayList<>(); + for ( ArtifactResult artifact : result.getArtifactResults() ) + { + File file = artifact.getArtifact().getFile(); + + URL url; + try + { + url = file.toURI().toURL(); + } catch ( MalformedURLException ex ) + { + throw new AssertionError( ex ); + } + + jarFiles.add( url ); + logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[] + { + desc.getName(), file + } ); + } + + URLClassLoader loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ) ); + + return loader; + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java index 75c75e2d..cda92c9c 100644 --- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java +++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java @@ -1,12 +1,23 @@ package net.md_5.bungee.api.plugin; import com.google.common.base.Preconditions; +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; +import java.security.CodeSigner; +import java.security.CodeSource; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import lombok.ToString; import net.md_5.bungee.api.ProxyServer; +@ToString(of = "desc") final class PluginClassloader extends URLClassLoader { @@ -14,6 +25,10 @@ final class PluginClassloader extends URLClassLoader // private final ProxyServer proxy; private final PluginDescription desc; + private final JarFile jar; + private final Manifest manifest; + private final URL url; + private final ClassLoader libraryLoader; // private Plugin plugin; @@ -22,11 +37,18 @@ final class PluginClassloader extends URLClassLoader ClassLoader.registerAsParallelCapable(); } - public PluginClassloader(ProxyServer proxy, PluginDescription desc, URL[] urls) + public PluginClassloader(ProxyServer proxy, PluginDescription desc, File file, ClassLoader libraryLoader) throws IOException { - super( urls ); + super( new URL[] + { + file.toURI().toURL() + } ); this.proxy = proxy; this.desc = desc; + this.jar = new JarFile( file ); + this.manifest = jar.getManifest(); + this.url = file.toURI().toURL(); + this.libraryLoader = libraryLoader; allLoaders.add( this ); } @@ -34,10 +56,10 @@ final class PluginClassloader extends URLClassLoader @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - return loadClass0( name, resolve, true ); + return loadClass0( name, resolve, true, true ); } - private Class loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException + private Class loadClass0(String name, boolean resolve, boolean checkOther, boolean checkLibraries) throws ClassNotFoundException { try { @@ -45,6 +67,17 @@ final class PluginClassloader extends URLClassLoader } catch ( ClassNotFoundException ex ) { } + + if ( checkLibraries && libraryLoader != null ) + { + try + { + return libraryLoader.loadClass( name ); + } catch ( ClassNotFoundException ex ) + { + } + } + if ( checkOther ) { for ( PluginClassloader loader : allLoaders ) @@ -53,13 +86,66 @@ final class PluginClassloader extends URLClassLoader { try { - return loader.loadClass0( name, resolve, false ); + return loader.loadClass0( name, resolve, false, proxy.getPluginManager().isTransitiveDepend( desc, loader.desc ) ); } catch ( ClassNotFoundException ex ) { } } } } + + throw new ClassNotFoundException( name ); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + String path = name.replace( '.', '/' ).concat( ".class" ); + JarEntry entry = jar.getJarEntry( path ); + + if ( entry != null ) + { + byte[] classBytes; + + try ( InputStream is = jar.getInputStream( entry ) ) + { + classBytes = ByteStreams.toByteArray( is ); + } catch ( IOException ex ) + { + throw new ClassNotFoundException( name, ex ); + } + + int dot = name.lastIndexOf( '.' ); + if ( dot != -1 ) + { + String pkgName = name.substring( 0, dot ); + if ( getPackage( pkgName ) == null ) + { + try + { + if ( manifest != null ) + { + definePackage( pkgName, manifest, url ); + } else + { + definePackage( pkgName, null, null, null, null, null, null, null ); + } + } catch ( IllegalArgumentException ex ) + { + if ( getPackage( pkgName ) == null ) + { + throw new IllegalStateException( "Cannot find package " + pkgName ); + } + } + } + } + + CodeSigner[] signers = entry.getCodeSigners(); + CodeSource source = new CodeSource( url, signers ); + + return defineClass( name, classBytes, 0, classBytes.length, source ); + } + throw new ClassNotFoundException( name ); } diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java index ef12ae90..ea5c0d37 100644 --- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java +++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java @@ -2,6 +2,8 @@ package net.md_5.bungee.api.plugin; import java.io.File; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import lombok.AllArgsConstructor; import lombok.Data; @@ -48,4 +50,8 @@ public class PluginDescription * Optional description. */ private String description = null; + /** + * Optional libraries. + */ + private List libraries = new LinkedList<>(); } diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java index b4da08dc..c122cad9 100644 --- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java +++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java @@ -4,10 +4,12 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.eventbus.Subscribe; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.Graphs; +import com.google.common.graph.MutableGraph; import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; -import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; import java.util.Collection; @@ -49,6 +51,8 @@ public final class PluginManager private final Yaml yaml; private final EventBus eventBus; private final Map plugins = new LinkedHashMap<>(); + private final MutableGraph dependencyGraph = GraphBuilder.directed().build(); + private final LibraryLoader libraryLoader; private final Map commandMap = new HashMap<>(); private Map toLoad = new HashMap<>(); private final Multimap commandsByPlugin = ArrayListMultimap.create(); @@ -67,6 +71,17 @@ public final class PluginManager yaml = new Yaml( yamlConstructor ); eventBus = new EventBus( proxy.getLogger() ); + + LibraryLoader libraryLoader = null; + try + { + libraryLoader = new LibraryLoader( proxy.getLogger() ); + } catch ( NoClassDefFoundError ex ) + { + // Provided depends were not added back + proxy.getLogger().warning( "Could not initialize LibraryLoader (missing dependencies?)" ); + } + this.libraryLoader = libraryLoader; } /** @@ -309,6 +324,7 @@ public final class PluginManager status = false; } + dependencyGraph.putEdge( plugin.getName(), dependName ); if ( !status ) { break; @@ -320,10 +336,7 @@ public final class PluginManager { try { - URLClassLoader loader = new PluginClassloader( proxy, plugin, new URL[] - { - plugin.getFile().toURI().toURL() - } ); + URLClassLoader loader = new PluginClassloader( proxy, plugin, plugin.getFile(), ( libraryLoader != null ) ? libraryLoader.createLoader( plugin ) : null ); Class main = loader.loadClass( plugin.getMain() ); Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance(); @@ -335,7 +348,7 @@ public final class PluginManager } ); } catch ( Throwable t ) { - proxy.getLogger().log( Level.WARNING, "Error enabling plugin " + plugin.getName(), t ); + proxy.getLogger().log( Level.WARNING, "Error loading plugin " + plugin.getName(), t ); } } @@ -463,4 +476,19 @@ public final class PluginManager { return Collections.unmodifiableCollection( commandMap.entrySet() ); } + + boolean isTransitiveDepend(PluginDescription plugin, PluginDescription depend) + { + Preconditions.checkArgument( plugin != null, "plugin" ); + Preconditions.checkArgument( depend != null, "depend" ); + + if ( dependencyGraph.nodes().contains( plugin.getName() ) ) + { + if ( Graphs.reachableNodes( dependencyGraph, plugin.getName() ).contains( depend.getName() ) ) + { + return true; + } + } + return false; + } } diff --git a/proxy/pom.xml b/proxy/pom.xml index 9f663469..61fb794d 100644 --- a/proxy/pom.xml +++ b/proxy/pom.xml @@ -91,6 +91,31 @@ 5.1.49 runtime + + + org.apache.maven + maven-resolver-provider + 3.8.1 + compile + + + org.apache.maven.resolver + maven-resolver-connector-basic + 1.6.2 + compile + + + org.apache.maven.resolver + maven-resolver-transport-http + 1.6.2 + compile + + + org.slf4j + slf4j-jdk14 + 1.7.30 + compile +