2020-08-18 14:18:12 +02:00
package net.minestom.server.extensions ;
2020-10-24 10:45:27 +02:00
import com.google.gson.Gson ;
2020-10-24 22:57:38 +02:00
import net.minestom.dependencies.DependencyGetter ;
2021-02-04 20:40:12 +01:00
import net.minestom.dependencies.ResolvedDependency ;
2020-10-24 22:57:38 +02:00
import net.minestom.dependencies.maven.MavenRepository ;
2022-01-05 09:01:21 +01:00
import net.minestom.server.ServerProcess ;
2020-10-17 16:30:37 +02:00
import net.minestom.server.utils.validate.Check ;
2021-12-04 03:33:39 +01:00
import org.jetbrains.annotations.ApiStatus ;
2020-08-18 14:18:12 +02:00
import org.jetbrains.annotations.NotNull ;
import org.jetbrains.annotations.Nullable ;
2020-11-16 03:10:51 +01:00
import org.slf4j.Logger ;
2020-08-18 14:18:12 +02:00
import org.slf4j.LoggerFactory ;
2020-10-30 18:43:14 +01:00
import java.io.* ;
2020-08-18 14:18:12 +02:00
import java.lang.reflect.Constructor ;
import java.lang.reflect.InvocationTargetException ;
import java.net.URL ;
2021-05-21 04:26:35 +02:00
import java.nio.file.Path ;
2020-10-30 18:43:14 +01:00
import java.util.* ;
2020-10-25 16:45:28 +01:00
import java.util.stream.Collectors ;
2021-06-20 00:12:56 +02:00
import java.util.zip.ZipEntry ;
2020-08-20 02:06:58 +02:00
import java.util.zip.ZipFile ;
2020-08-18 14:18:12 +02:00
public class ExtensionManager {
2020-11-16 03:10:51 +01:00
2020-11-18 09:24:59 +01:00
public final static Logger LOGGER = LoggerFactory . getLogger ( ExtensionManager . class ) ;
2021-02-03 17:11:06 +01:00
public final static String INDEV_CLASSES_FOLDER = " minestom.extension.indevfolder.classes " ;
public final static String INDEV_RESOURCES_FOLDER = " minestom.extension.indevfolder.resources " ;
2020-09-12 08:56:01 +02:00
private final static Gson GSON = new Gson ( ) ;
2020-10-17 16:30:37 +02:00
2022-01-05 09:01:21 +01:00
private final ServerProcess serverProcess ;
2021-03-24 20:53:02 +01:00
// LinkedHashMaps are HashMaps that preserve order
2021-03-24 13:48:55 +01:00
private final Map < String , Extension > extensions = new LinkedHashMap < > ( ) ;
private final Map < String , Extension > immutableExtensions = Collections . unmodifiableMap ( extensions ) ;
2021-03-24 13:39:47 +01:00
2022-12-31 03:52:16 +01:00
private final File extensionFolder = new File ( System . getProperty ( " minestom.extension.folder " , " extensions " ) ) ;
2020-10-24 22:57:38 +02:00
private final File dependenciesFolder = new File ( extensionFolder , " .libs " ) ;
2021-05-21 04:26:35 +02:00
private Path extensionDataRoot = extensionFolder . toPath ( ) ;
2021-12-04 03:46:14 +01:00
2022-01-05 09:01:21 +01:00
private enum State { DO_NOT_START , NOT_STARTED , STARTED , PRE_INIT , INIT , POST_INIT }
2021-12-04 03:46:14 +01:00
private State state = State . NOT_STARTED ;
2020-12-26 11:55:22 +01:00
2022-01-05 09:01:21 +01:00
public ExtensionManager ( ServerProcess serverProcess ) {
this . serverProcess = serverProcess ;
2020-08-18 14:18:12 +02:00
}
2020-12-26 11:55:22 +01:00
/ * *
* Gets if the extensions should be loaded during startup .
* < p >
* Default value is ' true ' .
*
2022-01-25 01:38:18 +01:00
* @return true if extensions are loaded in { @link net . minestom . server . MinecraftServer # start ( java . net . SocketAddress ) }
2020-12-26 11:55:22 +01:00
* /
public boolean shouldLoadOnStartup ( ) {
2021-12-04 03:46:14 +01:00
return state ! = State . DO_NOT_START ;
2020-12-26 11:55:22 +01:00
}
/ * *
* Used to specify if you want extensions to be loaded and initialized during startup .
* < p >
* Only useful before the server start .
*
* @param loadOnStartup true to load extensions on startup , false to do nothing
* /
public void setLoadOnStartup ( boolean loadOnStartup ) {
2021-12-04 03:46:14 +01:00
Check . stateCondition ( state . ordinal ( ) > State . NOT_STARTED . ordinal ( ) , " Extensions have already been initialized " ) ;
this . state = loadOnStartup ? State . NOT_STARTED : State . DO_NOT_START ;
2020-12-26 11:55:22 +01:00
}
2021-11-16 17:21:09 +01:00
@NotNull
public File getExtensionFolder ( ) {
return extensionFolder ;
}
public @NotNull Path getExtensionDataRoot ( ) {
return extensionDataRoot ;
}
public void setExtensionDataRoot ( @NotNull Path dataRoot ) {
this . extensionDataRoot = dataRoot ;
}
@NotNull
public Collection < Extension > getExtensions ( ) {
return immutableExtensions . values ( ) ;
}
@Nullable
public Extension getExtension ( @NotNull String name ) {
return extensions . get ( name . toLowerCase ( ) ) ;
}
public boolean hasExtension ( @NotNull String name ) {
return extensions . containsKey ( name ) ;
}
2021-12-04 03:33:39 +01:00
//
// Init phases
//
@ApiStatus.Internal
public void start ( ) {
2021-12-04 03:46:14 +01:00
if ( state = = State . DO_NOT_START ) {
2021-12-04 03:33:39 +01:00
LOGGER . warn ( " Extension loadOnStartup option is set to false, extensions are therefore neither loaded or initialized. " ) ;
return ;
}
2021-12-04 03:46:14 +01:00
Check . stateCondition ( state ! = State . NOT_STARTED , " ExtensionManager has already been started " ) ;
2021-12-04 03:33:39 +01:00
loadExtensions ( ) ;
2021-12-04 03:46:14 +01:00
state = State . STARTED ;
2021-12-04 03:33:39 +01:00
}
@ApiStatus.Internal
public void gotoPreInit ( ) {
2021-12-04 03:46:14 +01:00
if ( state = = State . DO_NOT_START ) return ;
Check . stateCondition ( state ! = State . STARTED , " Extensions have already done pre initialization " ) ;
2021-12-04 03:33:39 +01:00
extensions . values ( ) . forEach ( Extension : : preInitialize ) ;
2021-12-04 03:46:14 +01:00
state = State . PRE_INIT ;
2021-12-04 03:33:39 +01:00
}
@ApiStatus.Internal
public void gotoInit ( ) {
2021-12-04 03:46:14 +01:00
if ( state = = State . DO_NOT_START ) return ;
Check . stateCondition ( state ! = State . PRE_INIT , " Extensions have already done initialization " ) ;
2021-12-04 03:33:39 +01:00
extensions . values ( ) . forEach ( Extension : : initialize ) ;
2021-12-04 03:46:14 +01:00
state = State . INIT ;
2021-12-04 03:33:39 +01:00
}
@ApiStatus.Internal
public void gotoPostInit ( ) {
2021-12-04 03:46:14 +01:00
if ( state = = State . DO_NOT_START ) return ;
Check . stateCondition ( state ! = State . INIT , " Extensions have already done post initialization " ) ;
2021-12-04 03:33:39 +01:00
extensions . values ( ) . forEach ( Extension : : postInitialize ) ;
2021-12-04 03:46:14 +01:00
state = State . POST_INIT ;
2021-12-04 03:33:39 +01:00
}
2021-11-16 17:21:09 +01:00
//
// Loading
//
2021-03-24 16:55:46 +01:00
/ * *
* Loads all extensions in the extension folder into this server .
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* Pipeline :
2021-03-24 18:25:36 +01:00
* < br >
2021-03-24 16:55:46 +01:00
* Finds all . jar files in the extensions folder .
2021-03-24 18:25:36 +01:00
* < br >
2021-03-24 16:55:46 +01:00
* Per each jar :
2021-03-24 18:25:36 +01:00
* < br >
2021-03-24 16:55:46 +01:00
* Turns its extension . json into a DiscoveredExtension object .
2021-03-24 18:25:36 +01:00
* < br >
2021-03-24 16:55:46 +01:00
* Verifies that all properties of extension . json are correctly set .
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:58:51 +01:00
* It then sorts all those jars by their load order ( making sure that an extension ' s dependencies load before it )
2021-03-24 18:25:36 +01:00
* < br >
2021-03-24 16:58:51 +01:00
* Note : Cyclic dependencies will stop both extensions from being loaded .
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:58:51 +01:00
* Afterwards , it loads all external dependencies and adds them to the extension ' s files
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* Then removes any invalid extensions ( Invalid being its Load Status isn ' t SUCCESS )
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* After that , it set its classloaders so each extension is self - contained ,
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* Removes invalid extensions again ,
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* and loads all of those extensions into Minestom
2021-03-24 18:25:36 +01:00
* < br >
2021-03-24 16:55:46 +01:00
* ( Extension fields are set via reflection after each extension is verified , then loaded . )
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* If the extension successfully loads , add it to the global extension Map ( Name to Extension )
2021-03-24 18:25:36 +01:00
* < br > < br >
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:58:51 +01:00
* And finally make a scheduler to clean observers per extension .
2021-03-24 16:55:46 +01:00
* /
2021-12-04 03:33:39 +01:00
private void loadExtensions ( ) {
2021-03-24 16:55:46 +01:00
// Initialize folders
{
// Make extensions folder if necessary
if ( ! extensionFolder . exists ( ) ) {
if ( ! extensionFolder . mkdirs ( ) ) {
LOGGER . error ( " Could not find or create the extension folder, extensions will not be loaded! " ) ;
return ;
}
2020-08-18 14:18:12 +02:00
}
2021-03-24 16:55:46 +01:00
// Make dependencies folder if necessary
if ( ! dependenciesFolder . exists ( ) ) {
if ( ! dependenciesFolder . mkdirs ( ) ) {
LOGGER . error ( " Could not find nor create the extension dependencies folder, extensions will not be loaded! " ) ;
return ;
}
2020-10-24 22:57:38 +02:00
}
}
2021-03-24 16:55:46 +01:00
// Load extensions
{
// Get all extensions and order them accordingly.
List < DiscoveredExtension > discoveredExtensions = discoverExtensions ( ) ;
2021-03-24 17:23:44 +01:00
// Don't waste resources on doing extra actions if there is nothing to do.
if ( discoveredExtensions . isEmpty ( ) ) return ;
2021-11-16 18:08:15 +01:00
// Create classloaders for each extension (so that they can be used during dependency resolution)
Iterator < DiscoveredExtension > extensionIterator = discoveredExtensions . iterator ( ) ;
while ( extensionIterator . hasNext ( ) ) {
DiscoveredExtension discoveredExtension = extensionIterator . next ( ) ;
2021-03-24 16:55:46 +01:00
try {
2021-11-16 18:23:15 +01:00
discoveredExtension . createClassLoader ( ) ;
2021-03-24 16:55:46 +01:00
} catch ( Exception e ) {
discoveredExtension . loadStatus = DiscoveredExtension . LoadStatus . FAILED_TO_SETUP_CLASSLOADER ;
2022-01-05 09:01:21 +01:00
serverProcess . exception ( ) . handleException ( e ) ;
2021-03-24 16:55:46 +01:00
LOGGER . error ( " Failed to load extension {} " , discoveredExtension . getName ( ) ) ;
LOGGER . error ( " Failed to load extension " , e ) ;
2021-11-16 18:08:15 +01:00
extensionIterator . remove ( ) ;
2021-03-24 16:55:46 +01:00
}
2020-10-24 10:45:27 +02:00
}
2020-09-12 08:56:01 +02:00
2021-11-16 18:08:15 +01:00
discoveredExtensions = generateLoadOrder ( discoveredExtensions ) ;
loadDependencies ( discoveredExtensions ) ;
2021-03-24 16:55:46 +01:00
// remove invalid extensions
discoveredExtensions . removeIf ( ext - > ext . loadStatus ! = DiscoveredExtension . LoadStatus . LOAD_SUCCESS ) ;
2020-09-15 00:17:15 +02:00
2021-03-24 16:55:46 +01:00
// Load the extensions
for ( DiscoveredExtension discoveredExtension : discoveredExtensions ) {
try {
loadExtension ( discoveredExtension ) ;
} catch ( Exception e ) {
discoveredExtension . loadStatus = DiscoveredExtension . LoadStatus . LOAD_FAILED ;
LOGGER . error ( " Failed to load extension {} " , discoveredExtension . getName ( ) ) ;
2022-01-05 09:01:21 +01:00
serverProcess . exception ( ) . handleException ( e ) ;
2021-03-24 16:55:46 +01:00
}
2020-09-12 08:56:01 +02:00
}
2020-11-03 10:26:31 +01:00
}
}
2020-08-21 01:32:59 +02:00
2021-11-16 17:21:09 +01:00
public boolean loadDynamicExtension ( @NotNull File jarFile ) throws FileNotFoundException {
if ( ! jarFile . exists ( ) ) {
throw new FileNotFoundException ( " File ' " + jarFile . getAbsolutePath ( ) + " ' does not exists. Cannot load extension. " ) ;
}
LOGGER . info ( " Discover dynamic extension from jar {} " , jarFile . getAbsolutePath ( ) ) ;
DiscoveredExtension discoveredExtension = discoverFromJar ( jarFile ) ;
List < DiscoveredExtension > extensionsToLoad = Collections . singletonList ( discoveredExtension ) ;
return loadExtensionList ( extensionsToLoad ) ;
}
2021-03-24 16:55:46 +01:00
/ * *
* Loads an extension into Minestom .
*
* @param discoveredExtension The extension . Make sure to verify its integrity , set its class loader , and its files .
* @return An extension object made from this DiscoveredExtension
* /
2020-12-26 11:55:22 +01:00
@Nullable
2021-03-24 16:55:46 +01:00
private Extension loadExtension ( @NotNull DiscoveredExtension discoveredExtension ) {
2021-03-24 20:53:02 +01:00
// Create Extension (authors, version etc.)
2021-03-24 13:48:55 +01:00
String extensionName = discoveredExtension . getName ( ) ;
2020-11-03 10:26:31 +01:00
String mainClass = discoveredExtension . getEntrypoint ( ) ;
2021-11-21 21:11:50 +01:00
ExtensionClassLoader loader = discoveredExtension . getClassLoader ( ) ;
2020-11-03 10:26:31 +01:00
if ( extensions . containsKey ( extensionName . toLowerCase ( ) ) ) {
2020-11-18 09:24:59 +01:00
LOGGER . error ( " An extension called '{}' has already been registered. " , extensionName ) ;
2020-11-03 21:26:46 +01:00
return null ;
2020-11-03 10:26:31 +01:00
}
2020-08-18 14:18:12 +02:00
2020-11-03 10:26:31 +01:00
Class < ? > jarClass ;
try {
jarClass = Class . forName ( mainClass , true , loader ) ;
} catch ( ClassNotFoundException e ) {
2021-12-18 17:49:14 +01:00
LOGGER . error ( " Could not find main class '{}' in extension '{}'. " ,
2021-04-05 18:24:38 +02:00
mainClass , extensionName , e ) ;
2020-11-03 21:26:46 +01:00
return null ;
2020-08-18 14:18:12 +02:00
}
2020-11-03 10:26:31 +01:00
Class < ? extends Extension > extensionClass ;
try {
extensionClass = jarClass . asSubclass ( Extension . class ) ;
} catch ( ClassCastException e ) {
2020-11-18 09:24:59 +01:00
LOGGER . error ( " Main class '{}' in '{}' does not extend the 'Extension' superclass. " , mainClass , extensionName , e ) ;
2020-11-03 21:26:46 +01:00
return null ;
2020-11-03 10:26:31 +01:00
}
2020-08-18 14:18:12 +02:00
2020-11-03 10:26:31 +01:00
Constructor < ? extends Extension > constructor ;
try {
constructor = extensionClass . getDeclaredConstructor ( ) ;
// Let's just make it accessible, plugin creators don't have to make this public.
constructor . setAccessible ( true ) ;
} catch ( NoSuchMethodException e ) {
2020-11-18 09:24:59 +01:00
LOGGER . error ( " Main class '{}' in '{}' does not define a no-args constructor. " , mainClass , extensionName , e ) ;
2020-11-03 21:26:46 +01:00
return null ;
2020-11-03 10:26:31 +01:00
}
Extension extension = null ;
try {
extension = constructor . newInstance ( ) ;
} catch ( InstantiationException e ) {
2020-11-18 09:24:59 +01:00
LOGGER . error ( " Main class '{}' in '{}' cannot be an abstract class. " , mainClass , extensionName , e ) ;
2020-11-03 21:26:46 +01:00
return null ;
2020-11-03 10:26:31 +01:00
} catch ( IllegalAccessException ignored ) {
// We made it accessible, should not occur
} catch ( InvocationTargetException e ) {
2020-11-18 09:24:59 +01:00
LOGGER . error (
2020-11-03 10:26:31 +01:00
" While instantiating the main class '{}' in '{}' an exception was thrown. " ,
mainClass ,
extensionName ,
e . getTargetException ( )
) ;
2020-11-03 21:26:46 +01:00
return null ;
2020-11-03 10:26:31 +01:00
}
2020-10-24 10:45:27 +02:00
2020-11-03 21:26:46 +01:00
// add dependents to pre-existing extensions, so that they can easily be found during reloading
2021-03-24 17:16:29 +01:00
for ( String dependencyName : discoveredExtension . getDependencies ( ) ) {
Extension dependency = extensions . get ( dependencyName . toLowerCase ( ) ) ;
if ( dependency = = null ) {
LOGGER . warn ( " Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later. " , dependencyName , discoveredExtension . getName ( ) ) ;
2020-11-03 21:26:46 +01:00
} else {
2021-03-24 17:16:29 +01:00
dependency . getDependents ( ) . add ( discoveredExtension . getName ( ) ) ;
2020-11-03 21:26:46 +01:00
}
2020-08-18 14:18:12 +02:00
}
2020-11-03 21:26:46 +01:00
2021-03-24 20:53:02 +01:00
// add to a linked hash map, as they preserve order
2020-11-03 10:26:31 +01:00
extensions . put ( extensionName . toLowerCase ( ) , extension ) ;
2020-11-03 21:26:46 +01:00
return extension ;
2020-08-18 14:18:12 +02:00
}
2021-03-24 16:55:46 +01:00
/ * *
* Get all extensions from the extensions folder and make them discovered .
2021-04-05 18:24:38 +02:00
* < p >
2021-03-24 16:55:46 +01:00
* It skims the extension folder , discovers and verifies each extension , and returns those created DiscoveredExtensions .
*
* @return A list of discovered extensions from this folder .
* /
2021-09-05 03:40:30 +02:00
private @NotNull List < DiscoveredExtension > discoverExtensions ( ) {
2020-08-20 02:06:58 +02:00
List < DiscoveredExtension > extensions = new LinkedList < > ( ) ;
2021-03-24 00:59:40 +01:00
2021-02-17 17:37:54 +01:00
File [ ] fileList = extensionFolder . listFiles ( ) ;
2021-03-24 00:59:40 +01:00
if ( fileList ! = null ) {
// Loop through all files in extension folder
2021-02-17 17:37:54 +01:00
for ( File file : fileList ) {
2021-03-24 00:59:40 +01:00
// Ignore folders
2021-02-17 17:37:54 +01:00
if ( file . isDirectory ( ) ) {
continue ;
}
2021-03-24 00:59:40 +01:00
// Ignore non .jar files
2021-02-17 17:37:54 +01:00
if ( ! file . getName ( ) . endsWith ( " .jar " ) ) {
continue ;
}
2021-03-24 00:59:40 +01:00
2021-02-17 17:37:54 +01:00
DiscoveredExtension extension = discoverFromJar ( file ) ;
if ( extension ! = null & & extension . loadStatus = = DiscoveredExtension . LoadStatus . LOAD_SUCCESS ) {
extensions . add ( extension ) ;
}
2020-08-21 01:32:59 +02:00
}
}
2021-11-16 18:08:15 +01:00
//TODO(mattw): Extract this into its own method to load an extension given classes and resources directory.
//TODO(mattw): Should show a warning if one is set and not the other. It is most likely a mistake.
2020-08-21 01:32:59 +02:00
// this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder
2020-10-17 16:30:37 +02:00
if ( System . getProperty ( INDEV_CLASSES_FOLDER ) ! = null & & System . getProperty ( INDEV_RESOURCES_FOLDER ) ! = null ) {
2020-11-18 09:24:59 +01:00
LOGGER . info ( " Found indev folders for extension. Adding to list of discovered extensions. " ) ;
2020-10-17 16:54:35 +02:00
final String extensionClasses = System . getProperty ( INDEV_CLASSES_FOLDER ) ;
final String extensionResources = System . getProperty ( INDEV_RESOURCES_FOLDER ) ;
2020-10-17 16:30:37 +02:00
try ( InputStreamReader reader = new InputStreamReader ( new FileInputStream ( new File ( extensionResources , " extension.json " ) ) ) ) {
2020-10-24 22:57:38 +02:00
DiscoveredExtension extension = GSON . fromJson ( reader , DiscoveredExtension . class ) ;
2020-10-25 19:58:19 +01:00
extension . files . add ( new File ( extensionClasses ) . toURI ( ) . toURL ( ) ) ;
extension . files . add ( new File ( extensionResources ) . toURI ( ) . toURL ( ) ) ;
2021-05-21 04:26:35 +02:00
extension . setDataDirectory ( getExtensionDataRoot ( ) . resolve ( extension . getName ( ) ) ) ;
2020-10-25 10:41:51 +01:00
// Verify integrity and ensure defaults
DiscoveredExtension . verifyIntegrity ( extension ) ;
if ( extension . loadStatus = = DiscoveredExtension . LoadStatus . LOAD_SUCCESS ) {
2020-10-24 22:57:38 +02:00
extensions . add ( extension ) ;
}
2020-08-20 02:06:58 +02:00
} catch ( IOException e ) {
2022-01-05 09:01:21 +01:00
serverProcess . exception ( ) . handleException ( e ) ;
2020-08-20 02:06:58 +02:00
}
}
return extensions ;
}
2021-03-24 16:55:46 +01:00
/ * *
* Grabs a discovered extension from a jar .
*
* @param file The jar to grab it from ( a . jar is a formatted . zip file )
* @return The created DiscoveredExtension .
* /
2021-09-05 03:40:30 +02:00
private @Nullable DiscoveredExtension discoverFromJar ( @NotNull File file ) {
try ( ZipFile f = new ZipFile ( file ) ) {
2021-06-20 00:12:56 +02:00
ZipEntry entry = f . getEntry ( " extension.json " ) ;
if ( entry = = null )
throw new IllegalStateException ( " Missing extension.json in extension " + file . getName ( ) + " . " ) ;
InputStreamReader reader = new InputStreamReader ( f . getInputStream ( entry ) ) ;
2020-11-03 21:26:46 +01:00
2021-03-24 16:55:46 +01:00
// Initialize DiscoveredExtension from GSON.
2020-11-03 21:26:46 +01:00
DiscoveredExtension extension = GSON . fromJson ( reader , DiscoveredExtension . class ) ;
extension . setOriginalJar ( file ) ;
extension . files . add ( file . toURI ( ) . toURL ( ) ) ;
2021-05-21 04:26:35 +02:00
extension . setDataDirectory ( getExtensionDataRoot ( ) . resolve ( extension . getName ( ) ) ) ;
2020-11-03 21:26:46 +01:00
// Verify integrity and ensure defaults
DiscoveredExtension . verifyIntegrity ( extension ) ;
return extension ;
} catch ( IOException e ) {
2022-01-05 09:01:21 +01:00
serverProcess . exception ( ) . handleException ( e ) ;
2020-11-03 21:26:46 +01:00
return null ;
}
}
2021-03-23 16:35:52 +01:00
@NotNull
2020-12-26 11:55:22 +01:00
private List < DiscoveredExtension > generateLoadOrder ( @NotNull List < DiscoveredExtension > discoveredExtensions ) {
2021-03-24 01:13:15 +01:00
// Extension --> Extensions it depends on.
2020-10-25 16:45:28 +01:00
Map < DiscoveredExtension , List < DiscoveredExtension > > dependencyMap = new HashMap < > ( ) ;
2021-03-23 16:35:52 +01:00
2021-03-24 01:04:25 +01:00
// Put dependencies in dependency map
{
Map < String , DiscoveredExtension > extensionMap = new HashMap < > ( ) ;
2021-03-23 16:35:52 +01:00
2021-03-24 01:13:15 +01:00
// go through all the discovered extensions and assign their name in a map.
2021-03-24 01:04:25 +01:00
for ( DiscoveredExtension discoveredExtension : discoveredExtensions ) {
extensionMap . put ( discoveredExtension . getName ( ) . toLowerCase ( ) , discoveredExtension ) ;
}
2021-04-05 18:24:38 +02:00
allExtensions :
// go through all the discovered extensions and get their dependencies as extensions
2021-03-24 01:04:25 +01:00
for ( DiscoveredExtension discoveredExtension : discoveredExtensions ) {
List < DiscoveredExtension > dependencies = new ArrayList < > ( discoveredExtension . getDependencies ( ) . length ) ;
// Map the dependencies into DiscoveredExtensions.
for ( String dependencyName : discoveredExtension . getDependencies ( ) ) {
DiscoveredExtension dependencyExtension = extensionMap . get ( dependencyName . toLowerCase ( ) ) ;
// Specifies an extension we don't have.
if ( dependencyExtension = = null ) {
// attempt to see if it is not already loaded (happens with dynamic (re)loading)
if ( extensions . containsKey ( dependencyName . toLowerCase ( ) ) ) {
2020-10-25 16:45:28 +01:00
2021-03-24 01:04:25 +01:00
dependencies . add ( extensions . get ( dependencyName . toLowerCase ( ) ) . getOrigin ( ) ) ;
continue ; // Go to the next loop in this dependency loop, this iteration is done.
} else {
// dependency isn't loaded, move on.
LOGGER . error ( " Extension {} requires an extension called {}. " , discoveredExtension . getName ( ) , dependencyName ) ;
LOGGER . error ( " However the extension {} could not be found. " , dependencyName ) ;
LOGGER . error ( " Therefore {} will not be loaded. " , discoveredExtension . getName ( ) ) ;
discoveredExtension . loadStatus = DiscoveredExtension . LoadStatus . MISSING_DEPENDENCIES ;
continue allExtensions ; // the above labeled loop will go to the next extension as this dependency is invalid.
}
2021-03-24 00:59:40 +01:00
}
2021-03-24 01:04:25 +01:00
// This will add null for an unknown-extension
dependencies . add ( dependencyExtension ) ;
2021-03-24 00:59:40 +01:00
}
2021-03-24 01:04:25 +01:00
dependencyMap . put (
discoveredExtension ,
dependencies
) ;
2021-03-24 00:59:40 +01:00
2021-03-24 01:04:25 +01:00
}
2020-10-25 16:45:28 +01:00
}
2021-03-24 01:13:15 +01:00
// List containing the load order.
2020-10-25 16:45:28 +01:00
LinkedList < DiscoveredExtension > sortedList = new LinkedList < > ( ) ;
2021-03-24 01:13:15 +01:00
// TODO actually have to read this
{
// entries with empty lists
List < Map . Entry < DiscoveredExtension , List < DiscoveredExtension > > > loadableExtensions ;
// While there are entries with no more elements (no more dependencies)
while ( ! (
2021-12-13 16:41:30 +01:00
loadableExtensions = dependencyMap . entrySet ( ) . stream ( ) . filter ( entry - > isLoaded ( entry . getValue ( ) ) ) . toList ( )
2021-03-24 01:13:15 +01:00
) . isEmpty ( )
) {
// Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list.
2021-08-16 01:29:46 +02:00
for ( var entry : loadableExtensions ) {
2021-03-24 01:13:15 +01:00
// Add to sorted list.
sortedList . add ( entry . getKey ( ) ) ;
2021-08-16 01:29:46 +02:00
// Remove to make the next iterations a little quicker (hopefully) and to find cyclic dependencies.
2021-03-24 01:13:15 +01:00
dependencyMap . remove ( entry . getKey ( ) ) ;
// Remove this dependency from all the lists (if they include it) to make way for next level of extensions.
for ( var dependencies : dependencyMap . values ( ) ) {
dependencies . remove ( entry . getKey ( ) ) ;
}
2021-03-23 16:35:52 +01:00
}
2020-10-25 16:45:28 +01:00
}
}
// Check if there are cyclic extensions.
if ( ! dependencyMap . isEmpty ( ) ) {
2020-12-14 05:42:22 +01:00
LOGGER . error ( " Minestom found {} cyclic extensions. " , dependencyMap . size ( ) ) ;
2020-11-18 09:24:59 +01:00
LOGGER . error ( " Cyclic extensions depend on each other and can therefore not be loaded. " ) ;
2021-08-16 01:29:46 +02:00
for ( var entry : dependencyMap . entrySet ( ) ) {
2020-10-25 16:45:28 +01:00
DiscoveredExtension discoveredExtension = entry . getKey ( ) ;
2020-12-14 05:42:22 +01:00
LOGGER . error ( " {} could not be loaded, as it depends on: {}. " ,
discoveredExtension . getName ( ) ,
entry . getValue ( ) . stream ( ) . map ( DiscoveredExtension : : getName ) . collect ( Collectors . joining ( " , " ) ) ) ;
2020-10-25 16:45:28 +01:00
}
}
return sortedList ;
}
2021-03-24 16:55:46 +01:00
/ * *
* Checks if this list of extensions are loaded
2021-04-05 18:24:38 +02:00
*
2021-03-24 16:55:46 +01:00
* @param extensions The list of extensions to check against .
* @return If all of these extensions are loaded .
* /
private boolean isLoaded ( @NotNull List < DiscoveredExtension > extensions ) {
return
extensions . isEmpty ( ) // Don't waste CPU on checking an empty array
// Make sure the internal extensions list contains all of these.
| | extensions . stream ( ) . allMatch ( ext - > this . extensions . containsKey ( ext . getName ( ) . toLowerCase ( ) ) ) ;
2020-11-03 21:26:46 +01:00
}
2021-03-24 18:25:36 +01:00
private void loadDependencies ( @NotNull List < DiscoveredExtension > extensions ) {
2020-11-03 21:26:46 +01:00
List < DiscoveredExtension > allLoadedExtensions = new LinkedList < > ( extensions ) ;
2021-03-23 16:35:52 +01:00
2021-03-24 13:48:55 +01:00
for ( Extension extension : immutableExtensions . values ( ) )
2021-03-23 16:35:52 +01:00
allLoadedExtensions . add ( extension . getOrigin ( ) ) ;
for ( DiscoveredExtension discoveredExtension : extensions ) {
2020-10-25 16:45:28 +01:00
try {
DependencyGetter getter = new DependencyGetter ( ) ;
2021-03-23 16:35:52 +01:00
DiscoveredExtension . ExternalDependencies externalDependencies = discoveredExtension . getExternalDependencies ( ) ;
2020-10-25 16:45:28 +01:00
List < MavenRepository > repoList = new LinkedList < > ( ) ;
for ( var repository : externalDependencies . repositories ) {
2021-03-23 16:35:52 +01:00
2021-11-16 18:08:15 +01:00
if ( repository . name = = null | | repository . name . isEmpty ( ) ) {
2020-10-25 16:45:28 +01:00
throw new IllegalStateException ( " Missing 'name' element in repository object. " ) ;
}
2021-03-23 16:35:52 +01:00
2021-11-16 18:08:15 +01:00
if ( repository . url = = null | | repository . url . isEmpty ( ) ) {
2020-10-25 16:45:28 +01:00
throw new IllegalStateException ( " Missing 'url' element in repository object. " ) ;
}
2021-03-23 16:35:52 +01:00
2020-10-25 16:45:28 +01:00
repoList . add ( new MavenRepository ( repository . name , repository . url ) ) ;
}
2021-03-23 16:35:52 +01:00
2020-10-25 16:45:28 +01:00
getter . addMavenResolver ( repoList ) ;
2021-03-23 16:35:52 +01:00
for ( String artifact : externalDependencies . artifacts ) {
2020-10-25 16:45:28 +01:00
var resolved = getter . get ( artifact , dependenciesFolder ) ;
2021-03-23 16:35:52 +01:00
addDependencyFile ( resolved , discoveredExtension ) ;
LOGGER . trace ( " Dependency of extension {}: {} " , discoveredExtension . getName ( ) , resolved ) ;
2020-10-25 16:45:28 +01:00
}
2020-10-25 19:58:19 +01:00
2021-11-21 21:11:50 +01:00
ExtensionClassLoader extensionClassLoader = discoveredExtension . getClassLoader ( ) ;
2021-11-16 18:08:15 +01:00
for ( String dependencyName : discoveredExtension . getDependencies ( ) ) {
var resolved = extensions . stream ( )
. filter ( ext - > ext . getName ( ) . equalsIgnoreCase ( dependencyName ) )
2021-11-21 21:11:50 +01:00
. findFirst ( )
. orElseThrow ( ( ) - > new IllegalStateException ( " Unknown dependency ' " + dependencyName + " ' of ' " + discoveredExtension . getName ( ) + " ' " ) ) ;
2021-11-16 18:08:15 +01:00
2021-11-21 21:11:50 +01:00
ExtensionClassLoader dependencyClassLoader = resolved . getClassLoader ( ) ;
2021-11-16 18:23:15 +01:00
2021-11-16 18:08:15 +01:00
extensionClassLoader . addChild ( dependencyClassLoader ) ;
LOGGER . trace ( " Dependency of extension {}: {} " , discoveredExtension . getName ( ) , resolved ) ;
}
2020-10-25 16:45:28 +01:00
} catch ( Exception e ) {
2021-03-23 16:35:52 +01:00
discoveredExtension . loadStatus = DiscoveredExtension . LoadStatus . MISSING_DEPENDENCIES ;
LOGGER . error ( " Failed to load dependencies for extension {} " , discoveredExtension . getName ( ) ) ;
LOGGER . error ( " Extension '{}' will not be loaded " , discoveredExtension . getName ( ) ) ;
2020-11-18 09:24:59 +01:00
LOGGER . error ( " This is the exception " , e ) ;
2020-10-25 16:45:28 +01:00
}
}
}
2021-03-24 18:25:36 +01:00
private void addDependencyFile ( @NotNull ResolvedDependency dependency , @NotNull DiscoveredExtension extension ) {
2021-02-04 20:40:12 +01:00
URL location = dependency . getContentsLocation ( ) ;
extension . files . add ( location ) ;
2021-11-16 18:23:15 +01:00
extension . getClassLoader ( ) . addURL ( location ) ;
2021-02-04 20:40:12 +01:00
LOGGER . trace ( " Added dependency {} to extension {} classpath " , location . toExternalForm ( ) , extension . getName ( ) ) ;
// recurse to add full dependency tree
2021-03-23 16:42:32 +01:00
if ( ! dependency . getSubdependencies ( ) . isEmpty ( ) ) {
2021-02-04 20:40:12 +01:00
LOGGER . trace ( " Dependency {} has subdependencies, adding... " , location . toExternalForm ( ) ) ;
2021-03-23 16:42:32 +01:00
for ( ResolvedDependency sub : dependency . getSubdependencies ( ) ) {
2021-02-04 20:40:12 +01:00
addDependencyFile ( sub , extension ) ;
}
LOGGER . trace ( " Dependency {} has had its subdependencies added. " , location . toExternalForm ( ) ) ;
}
2020-10-25 16:45:28 +01:00
}
2021-03-24 18:25:36 +01:00
private boolean loadExtensionList ( @NotNull List < DiscoveredExtension > extensionsToLoad ) {
2020-11-06 16:03:08 +01:00
// ensure correct order of dependencies
2020-11-18 09:24:59 +01:00
LOGGER . debug ( " Reorder extensions to ensure proper load order " ) ;
2020-11-06 16:03:08 +01:00
extensionsToLoad = generateLoadOrder ( extensionsToLoad ) ;
loadDependencies ( extensionsToLoad ) ;
2020-11-03 21:26:46 +01:00
// setup new classloaders for the extensions to reload
2020-11-06 16:03:08 +01:00
for ( DiscoveredExtension toReload : extensionsToLoad ) {
2020-11-18 09:24:59 +01:00
LOGGER . debug ( " Setting up classloader for extension {} " , toReload . getName ( ) ) ;
2021-11-11 15:25:29 +01:00
// toReload.setMinestomExtensionClassLoader(toReload.makeClassLoader()); //TODO: Fix this
2020-11-03 21:26:46 +01:00
}
List < Extension > newExtensions = new LinkedList < > ( ) ;
2020-11-06 16:03:08 +01:00
for ( DiscoveredExtension toReload : extensionsToLoad ) {
2020-11-03 21:26:46 +01:00
// reload extensions
2020-11-18 09:24:59 +01:00
LOGGER . info ( " Actually load extension {} " , toReload . getName ( ) ) ;
2021-03-24 16:55:46 +01:00
Extension loadedExtension = loadExtension ( toReload ) ;
2020-11-06 23:35:31 +01:00
if ( loadedExtension ! = null ) {
2020-11-06 16:10:44 +01:00
newExtensions . add ( loadedExtension ) ;
2020-08-20 02:06:58 +02:00
}
}
2020-11-06 16:10:44 +01:00
2020-11-06 23:35:31 +01:00
if ( newExtensions . isEmpty ( ) ) {
2020-11-18 09:24:59 +01:00
LOGGER . error ( " No extensions to load, skipping callbacks " ) ;
2020-11-06 16:10:44 +01:00
return false ;
2020-11-03 21:26:46 +01:00
}
2020-11-18 09:24:59 +01:00
LOGGER . info ( " Load complete, firing preinit, init and then postinit callbacks " ) ;
2020-11-03 21:26:46 +01:00
// retrigger preinit, init and postinit
newExtensions . forEach ( Extension : : preInitialize ) ;
newExtensions . forEach ( Extension : : initialize ) ;
newExtensions . forEach ( Extension : : postInitialize ) ;
2020-11-06 16:10:44 +01:00
return true ;
2020-08-20 02:06:58 +02:00
}
2021-11-16 17:21:09 +01:00
//
// Shutdown / Unload
//
/ * *
* Shutdowns all the extensions by unloading them .
* /
public void shutdown ( ) { // copy names, as the extensions map will be modified via the calls to unload
Set < String > extensionNames = new HashSet < > ( extensions . keySet ( ) ) ;
for ( String ext : extensionNames ) {
if ( extensions . containsKey ( ext ) ) { // is still loaded? Because extensions can depend on one another, it might have already been unloaded
unloadExtension ( ext ) ;
}
}
}
private void unloadExtension ( @NotNull String extensionName ) {
2020-11-06 16:03:08 +01:00
Extension ext = extensions . get ( extensionName . toLowerCase ( ) ) ;
2021-03-24 16:55:46 +01:00
2020-11-06 23:35:31 +01:00
if ( ext = = null ) {
throw new IllegalArgumentException ( " Extension " + extensionName + " is not currently loaded. " ) ;
2020-11-06 16:03:08 +01:00
}
2021-03-24 16:55:46 +01:00
2021-03-23 16:35:52 +01:00
List < String > dependents = new LinkedList < > ( ext . getDependents ( ) ) ; // copy dependents list
2020-11-06 16:03:08 +01:00
2020-11-06 23:35:31 +01:00
for ( String dependentID : dependents ) {
2020-11-06 16:03:08 +01:00
Extension dependentExt = extensions . get ( dependentID . toLowerCase ( ) ) ;
2022-10-25 17:11:59 +02:00
if ( dependentExt ! = null ) { // check if extension isn't already unloaded.
2022-01-25 13:51:10 +01:00
LOGGER . info ( " Unloading dependent extension {} (because it depends on {}) " , dependentID , extensionName ) ;
unload ( dependentExt ) ;
}
2020-11-06 16:03:08 +01:00
}
2020-11-18 09:24:59 +01:00
LOGGER . info ( " Unloading extension {} " , extensionName ) ;
2020-11-06 16:03:08 +01:00
unload ( ext ) ;
2020-08-20 02:06:58 +02:00
}
2020-12-03 17:35:04 +01:00
2021-11-16 17:21:09 +01:00
private void unload ( @NotNull Extension ext ) {
ext . preTerminate ( ) ;
ext . terminate ( ) ;
2021-02-02 12:44:36 +01:00
2022-10-25 17:11:59 +02:00
ext . getExtensionClassLoader ( ) . terminate ( ) ;
2021-11-16 17:21:09 +01:00
ext . postTerminate ( ) ;
// remove from loaded extensions
String id = ext . getOrigin ( ) . getName ( ) . toLowerCase ( ) ;
extensions . remove ( id ) ;
// cleanup classloader
2021-11-16 18:23:15 +01:00
// TODO: Is it necessary to remove the CLs since this is only called on shutdown?
2021-02-03 20:56:36 +01:00
}
2020-08-18 14:18:12 +02:00
}