diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLClientLaunchProvider.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLClientLaunchProvider.java index 1d48b6002..3ee7cd3de 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLClientLaunchProvider.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLClientLaunchProvider.java @@ -22,21 +22,16 @@ package net.minecraftforge.fml.loading; import cpw.mods.modlauncher.api.IEnvironment; import cpw.mods.modlauncher.api.ILaunchHandlerService; import cpw.mods.modlauncher.api.ITransformingClassLoader; -import cpw.mods.modlauncher.api.ITransformingClassLoaderBuilder; import net.minecraftforge.api.distmarker.Dist; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; -import static net.minecraftforge.fml.loading.LogMarkers.CORE; - public class FMLClientLaunchProvider extends FMLCommonLaunchHandler implements ILaunchHandlerService { private static final Logger LOGGER = LogManager.getLogger(); @@ -88,4 +83,9 @@ public class FMLClientLaunchProvider extends FMLCommonLaunchHandler implements I public Path[] getPaths() { return FMLLoader.getMCPaths(); } + + @Override + public boolean isProduction() { + return true; + } } diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLCommonLaunchHandler.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLCommonLaunchHandler.java index 4b1a85b24..fb93e315d 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLCommonLaunchHandler.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLCommonLaunchHandler.java @@ -150,4 +150,8 @@ public abstract class FMLCommonLaunchHandler .peek(p->LOGGER.debug(LOADING, "Adding {} as a library to the transforming classloader", p)) .forEach(additionalLibraries::add); } + + public boolean isProduction() { + return false; + } } diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java index cfcb0d6e0..a31fa78c7 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java @@ -30,6 +30,7 @@ public class FMLEnvironment { public static final Dist dist = FMLLoader.getDist(); public static final String naming = FMLLoader.getNaming(); + public static final boolean production = FMLLoader.isProduction() || System.getProperties().containsKey("production"); static void setupInteropEnvironment(IEnvironment environment) { environment.computePropertyIfAbsent(IEnvironment.Keys.NAMING.get(), v->naming); diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLLoader.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLLoader.java index 8b6868c79..2d6615b10 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLLoader.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLLoader.java @@ -84,6 +84,7 @@ public class FMLLoader private static FMLCommonLaunchHandler commonLaunchHandler; public static Runnable progressWindowTick; public static BackgroundScanHandler backgroundScanHandler; + private static boolean production; static void onInitialLoad(IEnvironment environment, Set otherServices) throws IncompatibleEnvironmentException { @@ -181,6 +182,7 @@ public class FMLLoader commonLaunchHandler = (FMLCommonLaunchHandler)launchHandler.get(); naming = commonLaunchHandler.getNaming(); dist = commonLaunchHandler.getDist(); + production = commonLaunchHandler.isProduction(); progressWindowTick = EarlyProgressVisualization.INSTANCE.accept(dist); StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Early Loading!")); accessTransformer.getExtension().accept(Pair.of(naming, "srg")); @@ -296,4 +298,8 @@ public class FMLLoader public static String launcherHandlerName() { return launchHandlerName; } + + public static boolean isProduction() { + return production; + } } diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLServerLaunchProvider.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLServerLaunchProvider.java index 6645e0721..cbb719fdc 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLServerLaunchProvider.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLServerLaunchProvider.java @@ -83,4 +83,9 @@ public class FMLServerLaunchProvider extends FMLCommonLaunchHandler implements I public Path[] getPaths() { return FMLLoader.getMCPaths(); } + + @Override + public boolean isProduction() { + return true; + } } diff --git a/src/main/java/net/minecraftforge/fml/DistExecutor.java b/src/main/java/net/minecraftforge/fml/DistExecutor.java index a78027000..d7f5c2361 100644 --- a/src/main/java/net/minecraftforge/fml/DistExecutor.java +++ b/src/main/java/net/minecraftforge/fml/DistExecutor.java @@ -21,23 +21,51 @@ package net.minecraftforge.fml; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.loading.FMLEnvironment; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.io.Serializable; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.function.Supplier; +/** + * Use to execute code conditionally based on sidedness. + * + */ public final class DistExecutor { + private static final Logger LOGGER = LogManager.getLogger(); private DistExecutor() {} /** - * Run the callable in the supplier only on the specified {@link Side} + * Run the callable in the supplier only on the specified {@link Side}. + * This method is NOT sided-safe and special care needs to be taken in code using this method that implicit class + * loading is not triggered by the Callable. + * + * This method can cause unexpected ClassNotFound exceptions. + * + * Use {@link #safeCallWhenOn(Dist, Supplier)} where possible. * * @param dist The dist to run on * @param toRun A supplier of the callable to run (Supplier wrapper to ensure classloading only on the appropriate dist) * @param The return type from the callable * @return The callable's result + * @deprecated use {@link #safeCallWhenOn(Dist, Supplier)} instead. This remains for advanced use cases. */ + @Deprecated public static T callWhenOn(Dist dist, Supplier> toRun) { + return unsafeCallWhenOn(dist, toRun); + } + + public static T unsafeCallWhenOn(Dist dist, Supplier> toRun) { if (dist == FMLEnvironment.dist) { try { @@ -51,12 +79,95 @@ public final class DistExecutor return null; } + /** + * Call the SafeCallable when on the correct {@link Dist}. + * + * The lambda supplied here is required to be a method reference to a method defined in + * another class, otherwise an invalid SafeReferent error will be thrown + * @param dist the dist which this will run on + * @param toRun the SafeCallable to run and return the result from + * @param The type of the SafeCallable + * @return the result of the SafeCallable or null if on the wrong side + */ + public static T safeCallWhenOn(Dist dist, Supplier> toRun) { + validateSafeReferent(toRun); + return callWhenOn(dist, toRun::get); + } + + /** + * Runs the supplied Runnable on the speicified side. Same warnings apply as {@link #callWhenOn(Dist, Supplier)}. + * + * This method can cause unexpected ClassNotFound exceptions. + * + * @see #callWhenOn(Dist, Supplier) + * @param dist Dist to run this code on + * @param toRun The code to run + * @deprecated use {@link #safeRunWhenOn(Dist, Supplier)} where possible. Advanced uses only. + */ + @Deprecated public static void runWhenOn(Dist dist, Supplier toRun) { + unsafeRunWhenOn(dist, toRun); + } + /** + * Runs the supplied Runnable on the speicified side. Same warnings apply as {@link #unsafeCallWhenOn(Dist, Supplier)}. + * + * This method can cause unexpected ClassNotFoundException problems in common scenarios. Understand the pitfalls of + * the way the class verifier works to load classes before using this. + * + * Use {@link #safeRunWhenOn(Dist, Supplier)} if you can. + * + * @see #unsafeCallWhenOn(Dist, Supplier) + * @param dist Dist to run this code on + * @param toRun The code to run + */ + public static void unsafeRunWhenOn(Dist dist, Supplier toRun) { if (dist == FMLEnvironment.dist) { toRun.get().run(); } } + + /** + * Call the supplied SafeRunnable when on the correct Dist. + * @param dist The dist to run on + * @param toRun The code to run + */ + public static void safeRunWhenOn(Dist dist, Supplier toRun) { + validateSafeReferent(toRun); + if (dist == FMLEnvironment.dist) { + toRun.get().run(); + } + } + /** + * Executes one of the two suppliers, based on which side is active. + * + *

+ * Example (replacement for old SidedProxy):
+ * {@code Proxy p = DistExecutor.runForDist(()->ClientProxy::new, ()->ServerProxy::new);} + * + * NOTE: the double supplier is required to avoid classloading the secondary target. + * + * @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT} + * @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER} + * @param The common type to return + * @return The returned instance + * @deprecated Use {@link #safeRunForDist(Supplier, Supplier)} + */ + @Deprecated public static T runForDist(Supplier> clientTarget, Supplier> serverTarget) { + return unsafeRunForDist(clientTarget, serverTarget); + } + + /** + * Unsafe version of {@link #safeRunForDist(Supplier, Supplier)}. Use only when you know what you're doing + * and understand why the verifier can cause unexpected ClassNotFoundException crashes even when code is apparently + * not sided. Ensure you test both sides fully to be confident in using this. + * + * @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT} + * @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER} + * @param The common type to return + * @return The returned instance + */ + public static T unsafeRunForDist(Supplier> clientTarget, Supplier> serverTarget) { switch (FMLEnvironment.dist) { case CLIENT: @@ -67,4 +178,99 @@ public final class DistExecutor throw new IllegalArgumentException("UNSIDED?"); } } + /** + * Executes one of the two suppliers, based on which side is active. + * + *

+ * Example (replacement for old SidedProxy):
+ * {@code Proxy p = DistExecutor.safeRunForDist(()->ClientProxy::new, ()->ServerProxy::new);} + * + * NOTE: the double supplier is required to avoid classloading the secondary target. + * + * @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT} + * @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER} + * @param The common type to return + * @return The returned instance + */ + public static T safeRunForDist(Supplier> clientTarget, Supplier> serverTarget) { + validateSafeReferent(clientTarget); + validateSafeReferent(serverTarget); + switch (FMLEnvironment.dist) + { + case CLIENT: + return clientTarget.get().get(); + case DEDICATED_SERVER: + return serverTarget.get().get(); + default: + throw new IllegalArgumentException("UNSIDED?"); + } + } + + /** + * A safe referent. This will assert that it is being called via a separated class method reference. This will + * avoid the common pitfalls of {@link #callWhenOn(Dist, Supplier)} above. + * + * SafeReferents assert that they are defined as a separate method outside the scope of the calling class. + * + * Implementations need to be defined in a separate class to the referring site, with appropriate + * visibility to be accessible at the callsite (generally, avoid private methods). + * + *

+ * Valid:
+ * + * {@code DistExecutor.safeCallWhenOn(Dist.CLIENT, ()->AnotherClass::clientOnlyMethod);} + * + *

+ * Invalid:
+ * + * {@code DistExecutor.safeCallWhenOn(Dist.CLIENT, ()->()->Minecraft.getInstance().world);} + */ + public interface SafeReferent {} + + /** + * SafeCallable version of {@link SafeReferent}. + * @see SafeReferent + * @param The return type of the Callable + */ + public interface SafeCallable extends SafeReferent, Callable, Serializable {} + + /** + * SafeSupplier version of {@link SafeReferent} + * @param The return type of the Supplier + */ + public interface SafeSupplier extends SafeReferent, Supplier, Serializable {} + + /** + * SafeRunnable version of {@link SafeReferent} + */ + public interface SafeRunnable extends SafeReferent, Runnable, Serializable {} + + private static final void validateSafeReferent(Supplier safeReferentSupplier) { + if (FMLEnvironment.production) return; + final SafeReferent setter; + try { + setter = safeReferentSupplier.get(); + } catch (Exception e) { + // Typically a class cast exception, just return out, expected. + return; + } + + for (Class cl = setter.getClass(); cl != null; cl = cl.getSuperclass()) { + try { + Method m = cl.getDeclaredMethod("writeReplace"); + m.setAccessible(true); + Object replacement = m.invoke(setter); + if (!(replacement instanceof SerializedLambda)) + break;// custom interface implementation + SerializedLambda l = (SerializedLambda) replacement; + if (Objects.equals(l.getCapturingClass(), l.getImplClass())) { + LOGGER.fatal("Detected unsafe referent usage, please view the code at {}",Thread.currentThread().getStackTrace()[3]); + throw new RuntimeException("Unsafe Referent usage found in safe referent method"); + } + } catch (NoSuchMethodException e) { + } catch (IllegalAccessException | InvocationTargetException e) { + break; + } + } + } }