package net.minestom.server.extras.selfmodification; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.CodeSigner; import java.security.CodeSource; import java.util.ArrayList; import java.util.Locale; import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; import org.jetbrains.annotations.NotNull; import org.slf4j.LoggerFactory; import de.geolykt.starloader.launcher.Utils; import de.geolykt.starloader.util.JavaInterop; public class MinestomExtensionClassLoader extends HierarchyClassLoader { /** * Root ClassLoader, everything goes through it before any attempt at loading is done inside this classloader. */ private final MinestomRootClassLoader root; private static final boolean DEBUG = Boolean.getBoolean("classloader.debug"); private static final boolean DUMP = DEBUG || Boolean.getBoolean("classloader.dump"); public MinestomExtensionClassLoader(String name, URL[] urls, MinestomRootClassLoader root) { super(name, urls, root); this.root = root; } @Override public void addURL(URL url) { super.addURL(url); } @Override public void close() throws IOException { synchronized (HierarchyClassLoader.class) { for (HierarchyClassLoader parent : this.parents) { parent.removeChildInHierarchy(this); } this.parents.clear(); for (MinestomExtensionClassLoader cl : new ArrayList<>(this.children)) { LoggerFactory.getLogger(MinestomExtensionClassLoader.class).info("Closing classloader {} as it is a child of classloader {}, which is getting closed", cl.getName(), this.getName()); cl.close(); } } super.close(); } @Override @NotNull public Class loadClass(@NotNull String name) throws ClassNotFoundException { return this.loadClass(name, false); } @Override @NotNull protected Class loadClass(@NotNull String name, boolean resolve) throws ClassNotFoundException { try { return this.loadClassAsChild(name, resolve); } catch (ClassNotFoundException cnfe) { try { return this.root.loadClass(name, resolve); } catch (ClassNotFoundException e) { e.addSuppressed(cnfe); throw e; } } } /** * Assumes the name is not null, nor it does represent a protected class. * * @param name * @return The loaded class * @throws ClassNotFoundException if the class is not found inside this classloader */ @NotNull public Class loadClassAsChild(String name, boolean resolve) throws ClassNotFoundException { Class loadedClass = this.findLoadedClass(name); if (loadedClass != null) { return loadedClass; } if (this.root.isProtected(name)) { throw new ClassNotFoundException("The MinestomExtensionClassLoader is not permitted to load this class as it is protected by the root classloader."); } try { // not in children, attempt load in this classloader String path = name.replace(".", "/") + ".class"; URL url = this.findResource(path); if (url == null) { throw new ClassNotFoundException("Could not find class " + name); } try (InputStream in = url.openStream()) { if (in == null) { throw new AssertionError(); } byte[] bytes = JavaInterop.readAllBytes(in); bytes = this.root.transformBytes(bytes, name, Utils.toCodeSourceURI(url, name)); if (MinestomExtensionClassLoader.DUMP) { Path parent = Paths.get("classes", path).getParent(); if (parent != null) { Files.createDirectories(parent); } Files.write(Paths.get("classes", path), bytes); } String urlPath = url.getPath(); int seperatorIndex = urlPath.lastIndexOf('!'); if (seperatorIndex != -1) { url = new URL(urlPath.substring(0, seperatorIndex)); } Class clazz = super.defineClass(name, bytes, 0, bytes.length, new CodeSource(url, (CodeSigner[]) null)); if (resolve) { super.resolveClass(clazz); } return clazz; } catch (Throwable e) { throw new ClassNotFoundException("Could not load class " + name, e); } } catch (ClassNotFoundException e) { for (MinestomExtensionClassLoader child : this.children) { try { Class loaded = child.loadClassAsChild(name, resolve); return loaded; } catch (ClassNotFoundException e1) { // move on to next child e.addSuppressed(e1); } } throw e; } } @Override @Deprecated @ScheduledForRemoval protected void finalize() throws Throwable { super.finalize(); System.err.println("Class loader " + this.getName() + " finalized."); } @Override public String toString() { return "Extension classloader '" + this.getName() + "'@" + Integer.toHexString(this.hashCode()).toUpperCase(Locale.ROOT); } static { ClassLoader.registerAsParallelCapable(); } }