diff --git a/examples/assets/placeholdername/blockstates/amongium.json b/examples/assets/groovyscriptdev/blockstates/amongium.json similarity index 100% rename from examples/assets/placeholdername/blockstates/amongium.json rename to examples/assets/groovyscriptdev/blockstates/amongium.json diff --git a/examples/assets/placeholdername/blockstates/dragon_egg.json b/examples/assets/groovyscriptdev/blockstates/dragon_egg.json similarity index 50% rename from examples/assets/placeholdername/blockstates/dragon_egg.json rename to examples/assets/groovyscriptdev/blockstates/dragon_egg.json index e10df24ce..d6c7fcb74 100644 --- a/examples/assets/placeholdername/blockstates/dragon_egg.json +++ b/examples/assets/groovyscriptdev/blockstates/dragon_egg.json @@ -1,7 +1,7 @@ { "variants": { "normal": { - "model": "placeholdername:dragon_egg" + "model": "groovyscriptdev:dragon_egg" } } } \ No newline at end of file diff --git a/examples/assets/groovyscriptdev/blockstates/dragon_egg_lamp.json b/examples/assets/groovyscriptdev/blockstates/dragon_egg_lamp.json new file mode 100644 index 000000000..e61545f1d --- /dev/null +++ b/examples/assets/groovyscriptdev/blockstates/dragon_egg_lamp.json @@ -0,0 +1,7 @@ +{ + "variants": { + "normal": { + "model": "groovyscriptdev:dragon_egg_lamp" + } + } +} \ No newline at end of file diff --git a/examples/assets/groovyscriptdev/blockstates/generic_block.json b/examples/assets/groovyscriptdev/blockstates/generic_block.json new file mode 100644 index 000000000..4b7e82e31 --- /dev/null +++ b/examples/assets/groovyscriptdev/blockstates/generic_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "normal": { + "model": "groovyscriptdev:generic_block" + } + } +} \ No newline at end of file diff --git a/examples/assets/placeholdername/lang/en_us.lang b/examples/assets/groovyscriptdev/lang/en_us.lang similarity index 100% rename from examples/assets/placeholdername/lang/en_us.lang rename to examples/assets/groovyscriptdev/lang/en_us.lang diff --git a/examples/assets/placeholdername/models/block/dragon_egg_lamp.json b/examples/assets/groovyscriptdev/models/block/dragon_egg_lamp.json similarity index 51% rename from examples/assets/placeholdername/models/block/dragon_egg_lamp.json rename to examples/assets/groovyscriptdev/models/block/dragon_egg_lamp.json index 24dd3f896..f4e7bb6d4 100644 --- a/examples/assets/placeholdername/models/block/dragon_egg_lamp.json +++ b/examples/assets/groovyscriptdev/models/block/dragon_egg_lamp.json @@ -1,6 +1,6 @@ { "parent": "block/dragon_egg", "textures": { - "all": "placeholdername:blocks/dragon_egg_lamp" + "all": "groovyscriptdev:blocks/dragon_egg_lamp" } } \ No newline at end of file diff --git a/examples/assets/placeholdername/models/block/generic_block.json b/examples/assets/groovyscriptdev/models/block/generic_block.json similarity index 51% rename from examples/assets/placeholdername/models/block/generic_block.json rename to examples/assets/groovyscriptdev/models/block/generic_block.json index 3c4e7b328..e8046b380 100644 --- a/examples/assets/placeholdername/models/block/generic_block.json +++ b/examples/assets/groovyscriptdev/models/block/generic_block.json @@ -1,6 +1,6 @@ { "parent": "block/cube_all", "textures": { - "all": "placeholdername:blocks/generic_block" + "all": "groovyscriptdev:blocks/generic_block" } } \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/clay_2.json b/examples/assets/groovyscriptdev/models/item/clay_2.json similarity index 54% rename from examples/assets/placeholdername/models/item/clay_2.json rename to examples/assets/groovyscriptdev/models/item/clay_2.json index 73272f6ed..7cc85e2e5 100644 --- a/examples/assets/placeholdername/models/item/clay_2.json +++ b/examples/assets/groovyscriptdev/models/item/clay_2.json @@ -1,6 +1,6 @@ { "parent": "item/generated", "textures": { - "layer0": "placeholdername:items/clay_2" + "layer0": "groovyscriptdev:items/clay_2" } } \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/clay_3.json b/examples/assets/groovyscriptdev/models/item/clay_3.json similarity index 54% rename from examples/assets/placeholdername/models/item/clay_3.json rename to examples/assets/groovyscriptdev/models/item/clay_3.json index 96d36b2b8..0a1783c6b 100644 --- a/examples/assets/placeholdername/models/item/clay_3.json +++ b/examples/assets/groovyscriptdev/models/item/clay_3.json @@ -1,6 +1,6 @@ { "parent": "item/generated", "textures": { - "layer0": "placeholdername:items/clay_3" + "layer0": "groovyscriptdev:items/clay_3" } } \ No newline at end of file diff --git a/examples/assets/groovyscriptdev/models/item/dragon_egg_lamp.json b/examples/assets/groovyscriptdev/models/item/dragon_egg_lamp.json new file mode 100644 index 000000000..e1d2b2a39 --- /dev/null +++ b/examples/assets/groovyscriptdev/models/item/dragon_egg_lamp.json @@ -0,0 +1,3 @@ +{ + "parent": "groovyscriptdev:block/dragon_egg_lamp" +} \ No newline at end of file diff --git a/examples/assets/groovyscriptdev/models/item/generic_block.json b/examples/assets/groovyscriptdev/models/item/generic_block.json new file mode 100644 index 000000000..2bc77231b --- /dev/null +++ b/examples/assets/groovyscriptdev/models/item/generic_block.json @@ -0,0 +1,3 @@ +{ + "parent": "groovyscriptdev:block/generic_block" +} \ No newline at end of file diff --git a/examples/assets/groovyscriptdev/models/item/heartofauniverse.json b/examples/assets/groovyscriptdev/models/item/heartofauniverse.json new file mode 100644 index 000000000..ec7469b28 --- /dev/null +++ b/examples/assets/groovyscriptdev/models/item/heartofauniverse.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "groovyscriptdev:items/heartofauniverse" + } +} \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/prodigy_stick.json b/examples/assets/groovyscriptdev/models/item/prodigy_stick.json similarity index 50% rename from examples/assets/placeholdername/models/item/prodigy_stick.json rename to examples/assets/groovyscriptdev/models/item/prodigy_stick.json index 10bfc9abb..6b7b44b27 100644 --- a/examples/assets/placeholdername/models/item/prodigy_stick.json +++ b/examples/assets/groovyscriptdev/models/item/prodigy_stick.json @@ -1,6 +1,6 @@ { "parent": "item/generated", "textures": { - "layer0": "placeholdername:items/prodigy_stick" + "layer0": "groovyscriptdev:items/prodigy_stick" } } \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/snack.json b/examples/assets/groovyscriptdev/models/item/snack.json similarity index 54% rename from examples/assets/placeholdername/models/item/snack.json rename to examples/assets/groovyscriptdev/models/item/snack.json index 758109e6b..24feccbaf 100644 --- a/examples/assets/placeholdername/models/item/snack.json +++ b/examples/assets/groovyscriptdev/models/item/snack.json @@ -1,6 +1,6 @@ { "parent": "item/generated", "textures": { - "layer0": "placeholdername:items/snack" + "layer0": "groovyscriptdev:items/snack" } } \ No newline at end of file diff --git a/examples/assets/placeholdername/textures/blocks/dragon_egg_lamp.png b/examples/assets/groovyscriptdev/textures/blocks/dragon_egg_lamp.png similarity index 100% rename from examples/assets/placeholdername/textures/blocks/dragon_egg_lamp.png rename to examples/assets/groovyscriptdev/textures/blocks/dragon_egg_lamp.png diff --git a/examples/assets/placeholdername/textures/blocks/generic_block.png b/examples/assets/groovyscriptdev/textures/blocks/generic_block.png similarity index 100% rename from examples/assets/placeholdername/textures/blocks/generic_block.png rename to examples/assets/groovyscriptdev/textures/blocks/generic_block.png diff --git a/examples/assets/placeholdername/textures/blocks/mekanism_infusion_texture.png b/examples/assets/groovyscriptdev/textures/blocks/mekanism_infusion_texture.png similarity index 100% rename from examples/assets/placeholdername/textures/blocks/mekanism_infusion_texture.png rename to examples/assets/groovyscriptdev/textures/blocks/mekanism_infusion_texture.png diff --git a/examples/assets/placeholdername/textures/items/clay_2.png b/examples/assets/groovyscriptdev/textures/items/clay_2.png similarity index 100% rename from examples/assets/placeholdername/textures/items/clay_2.png rename to examples/assets/groovyscriptdev/textures/items/clay_2.png diff --git a/examples/assets/placeholdername/textures/items/clay_3.png b/examples/assets/groovyscriptdev/textures/items/clay_3.png similarity index 100% rename from examples/assets/placeholdername/textures/items/clay_3.png rename to examples/assets/groovyscriptdev/textures/items/clay_3.png diff --git a/examples/assets/placeholdername/textures/items/heartofauniverse.png b/examples/assets/groovyscriptdev/textures/items/heartofauniverse.png similarity index 100% rename from examples/assets/placeholdername/textures/items/heartofauniverse.png rename to examples/assets/groovyscriptdev/textures/items/heartofauniverse.png diff --git a/examples/assets/placeholdername/textures/items/heartofauniverse.png.mcmeta b/examples/assets/groovyscriptdev/textures/items/heartofauniverse.png.mcmeta similarity index 100% rename from examples/assets/placeholdername/textures/items/heartofauniverse.png.mcmeta rename to examples/assets/groovyscriptdev/textures/items/heartofauniverse.png.mcmeta diff --git a/examples/assets/placeholdername/textures/items/prodigy_stick.png b/examples/assets/groovyscriptdev/textures/items/prodigy_stick.png similarity index 100% rename from examples/assets/placeholdername/textures/items/prodigy_stick.png rename to examples/assets/groovyscriptdev/textures/items/prodigy_stick.png diff --git a/examples/assets/placeholdername/textures/items/snack.png b/examples/assets/groovyscriptdev/textures/items/snack.png similarity index 100% rename from examples/assets/placeholdername/textures/items/snack.png rename to examples/assets/groovyscriptdev/textures/items/snack.png diff --git a/examples/assets/placeholdername/blockstates/dragon_egg_lamp.json b/examples/assets/placeholdername/blockstates/dragon_egg_lamp.json deleted file mode 100644 index d575e5727..000000000 --- a/examples/assets/placeholdername/blockstates/dragon_egg_lamp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "variants": { - "normal": { - "model": "placeholdername:dragon_egg_lamp" - } - } -} \ No newline at end of file diff --git a/examples/assets/placeholdername/blockstates/generic_block.json b/examples/assets/placeholdername/blockstates/generic_block.json deleted file mode 100644 index 7b1e585c7..000000000 --- a/examples/assets/placeholdername/blockstates/generic_block.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "variants": { - "normal": { - "model": "placeholdername:generic_block" - } - } -} \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/dragon_egg_lamp.json b/examples/assets/placeholdername/models/item/dragon_egg_lamp.json deleted file mode 100644 index 1fb046c4e..000000000 --- a/examples/assets/placeholdername/models/item/dragon_egg_lamp.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "placeholdername:block/dragon_egg_lamp" -} \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/generic_block.json b/examples/assets/placeholdername/models/item/generic_block.json deleted file mode 100644 index 8b218e06f..000000000 --- a/examples/assets/placeholdername/models/item/generic_block.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "placeholdername:block/generic_block" -} \ No newline at end of file diff --git a/examples/assets/placeholdername/models/item/heartofauniverse.json b/examples/assets/placeholdername/models/item/heartofauniverse.json deleted file mode 100644 index 767d717d1..000000000 --- a/examples/assets/placeholdername/models/item/heartofauniverse.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "item/generated", - "textures": { - "layer0": "placeholdername:items/heartofauniverse" - } -} \ No newline at end of file diff --git a/examples/postInit/betterwithaddons.groovy b/examples/postInit/betterwithaddons.groovy index 09363b5b0..f99d870f2 100644 --- a/examples/postInit/betterwithaddons.groovy +++ b/examples/postInit/betterwithaddons.groovy @@ -103,6 +103,31 @@ mods.betterwithaddons.lure_tree.recipeBuilder() mods.betterwithaddons.lure_tree.addBlacklist(entity('minecraft:chicken')) +// Packing: +// Converts an input itemstack in the form of a EntityItems into an IBlockState after a piston extends if the piston and +// location the EntityItems are in are fully surrounded by solid blocks. + +mods.betterwithaddons.packing.removeByInput(item('minecraft:clay_ball')) +mods.betterwithaddons.packing.removeByOutput(blockstate('minecraft:gravel')) +// mods.betterwithaddons.packing.removeAll() + +mods.betterwithaddons.packing.recipeBuilder() + .input(item('minecraft:gold_ingot')) + .compress(blockstate('minecraft:clay')) + .register() + +mods.betterwithaddons.packing.recipeBuilder() + .input(item('minecraft:clay') * 10) + .compress(blockstate('minecraft:diamond_block')) + .register() + +mods.betterwithaddons.packing.recipeBuilder() + .input(item('minecraft:diamond')) + .compress(blockstate('minecraft:dirt')) + .jeiOutput(item('minecraft:diamond') * 64) + .register() + + // Rotting Food: // Converts an input item into an output itemstack after the given time has passed. Has the ability to customize the // terminology used to indicate the age. diff --git a/examples/postInit/thebetweenlands.groovy b/examples/postInit/thebetweenlands.groovy index c910efbfd..fa1addab5 100644 --- a/examples/postInit/thebetweenlands.groovy +++ b/examples/postInit/thebetweenlands.groovy @@ -206,4 +206,3 @@ mods.thebetweenlands.steeping_pot.recipeBuilder() mods.thebetweenlands.steeping_pot.addAcceptedItem(item('minecraft:gold_block')) - diff --git a/examples/runConfig.json b/examples/runConfig.json index 6c7e947fd..c807aeeaa 100644 --- a/examples/runConfig.json +++ b/examples/runConfig.json @@ -1,18 +1,19 @@ { - "packName": "PlaceHolder name", - "packId": "placeholdername", + "packName": "GroovyScript Dev", + "packId": "groovyscriptdev", "version": "1.0.0", "debug": true, - "classes": [ - "classes/" - ], "loaders": { "preInit": [ + "classes/", "preInit/" ], - "init": [], + "init": [ + "init/" + ], "postInit": [ - "postInit/" + "postInit/", + "recipes/" ] }, "packmode": { diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index b83f9dda0..f40d97012 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -261,7 +261,7 @@ private static RunConfig createRunConfig(JsonObject json) { if (!Files.exists(main.toPath())) { try { main.getParentFile().mkdirs(); - Files.write(main.toPath(), "\nprintln('Hello World!')\n".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE); + Files.write(main.toPath(), "\nlog.info('Hello World!')\n".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java b/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java index f5023df9f..6a69ffaa8 100644 --- a/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java +++ b/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java @@ -102,7 +102,7 @@ public GSCommand() { })); addSubcommand(new SimpleCommand("deleteScriptCache", (server, sender, args) -> { - if (GroovyScript.getSandbox().deleteScriptCache()) { + if (GroovyScript.getSandbox().getEngine().deleteScriptCache()) { sender.sendMessage(new TextComponentString("Deleted groovy script cache").setStyle(StyleConstant.getSuccessStyle())); } else { sender.sendMessage(new TextComponentString("An error occurred while deleting groovy script cache").setStyle(StyleConstant.getErrorStyle())); diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ClassCollectorMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ClassCollectorMixin.java deleted file mode 100644 index 6d2757f76..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ClassCollectorMixin.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.cleanroommc.groovyscript.core.mixin.groovy; - -import com.cleanroommc.groovyscript.GroovyScript; -import groovy.lang.GroovyClassLoader; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.control.SourceUnit; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(value = GroovyClassLoader.ClassCollector.class, remap = false) -public class ClassCollectorMixin { - - @Shadow - @Final - private SourceUnit su; - - @Inject(method = "createClass", at = @At("RETURN")) - public void onCreateClass(byte[] code, ClassNode classNode, CallbackInfoReturnable> cir) { - GroovyScript.getSandbox().onCompileClass(su, su.getName(), cir.getReturnValue(), code, classNode.getName().contains("$")); - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/GroovyClassLoaderMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/GroovyClassLoaderMixin.java deleted file mode 100644 index 3e1ab7182..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/GroovyClassLoaderMixin.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.cleanroommc.groovyscript.core.mixin.groovy; - -import com.cleanroommc.groovyscript.GroovyScript; -import groovy.lang.GroovyClassLoader; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.net.URL; - -/** - * If a script depends on another script and the there is a compiled cache for the script, it needs to be loaded manually. - */ -@Mixin(value = GroovyClassLoader.class, remap = false) -public class GroovyClassLoaderMixin { - - @Inject(method = "recompile", at = @At("HEAD"), cancellable = true) - public void onRecompile(URL source, String className, Class oldClass, CallbackInfoReturnable> cir) { - if (source != null && oldClass == null) { - Class c = GroovyScript.getSandbox().onRecompileClass(source, className); - if (c != null) { - cir.setReturnValue(c); - } - } - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java b/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java index 2c7533c76..b7e319bcd 100644 --- a/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java +++ b/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java @@ -24,7 +24,8 @@ public static LoadStage getLoadStage() { } public static boolean isReloading() { - return getLoadStage().isReloadable() && !ReloadableRegistryManager.isFirstLoad(); + LoadStage loadStage = getLoadStage(); + return loadStage != null && loadStage.isReloadable() && !ReloadableRegistryManager.isFirstLoad(); } public static String getMinecraftVersion() { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CachedClassLoader.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/CachedClassLoader.java deleted file mode 100644 index 89f759427..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CachedClassLoader.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.cleanroommc.groovyscript.sandbox; - -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import net.minecraft.launchwrapper.Launch; - -import java.util.Map; - -public class CachedClassLoader extends ClassLoader { - - private final Map> cache = new Object2ObjectOpenHashMap<>(); - - public CachedClassLoader() { - super(Launch.classLoader); - } - - public Class defineClass(String name, byte[] bytes) { - Class clz = super.defineClass(name, bytes, 0, bytes.length); - resolveClass(clz); - this.cache.put(clz.getName(), clz); - return clz; - } - - public void clearCache() { - this.cache.clear(); - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - Class clz = tryLoadClass(name); - if (clz != null) return clz; - return super.loadClass(name, resolve); - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - Class clz = tryLoadClass(name); - if (clz != null) return clz; - return super.findClass(name); - } - - public Class tryLoadClass(String name) { - Class clz = this.cache.get(name); - if (clz != null) return clz; - try { - return Launch.classLoader.findClass(name); - } catch (ClassNotFoundException e) { - return null; - } - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java index 0ca08784d..c65240e41 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java @@ -1,12 +1,15 @@ package com.cleanroommc.groovyscript.sandbox; import com.cleanroommc.groovyscript.api.GroovyLog; +import groovy.lang.GroovyClassLoader; import org.apache.commons.lang3.builder.ToStringBuilder; +import org.codehaus.groovy.runtime.InvokerHelper; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; +import java.util.Map; class CompiledClass { @@ -29,12 +32,13 @@ public void onCompile(byte[] data, Class clazz, String basePath) { public void onCompile(Class clazz, String basePath) { this.clazz = clazz; - this.name = clazz.getName(); + if (!this.name.equals(clazz.getName())) throw new IllegalArgumentException(); + //this.name = clazz.getName(); if (this.data == null) { GroovyLog.get().errorMC("The class doesnt seem to be compiled yet. (" + name + ")"); return; } - if (!GroovyScriptSandbox.ENABLE_CACHE) return; + if (!CustomGroovyScriptEngine.ENABLE_CACHE) return; try { File file = getDataFile(basePath); file.getParentFile().mkdirs(); @@ -47,14 +51,15 @@ public void onCompile(Class clazz, String basePath) { } } - protected void ensureLoaded(CachedClassLoader classLoader, String basePath) { + protected void ensureLoaded(GroovyClassLoader classLoader, Map cache, String basePath) { if (this.clazz == null) { this.clazz = classLoader.defineClass(this.name, this.data); + cache.put(this.name, this); } } public boolean readData(String basePath) { - if (this.data != null && GroovyScriptSandbox.ENABLE_CACHE) return true; + if (this.data != null && CustomGroovyScriptEngine.ENABLE_CACHE) return true; File file = getDataFile(basePath); if (!file.exists()) return false; try { @@ -71,6 +76,11 @@ public void deleteCache(String cachePath) { } catch (IOException e) { throw new RuntimeException(e); } + if (this.clazz != null) { + InvokerHelper.removeClass(this.clazz); + this.clazz = null; + } + this.data = null; } protected File getDataFile(String basePath) { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java index 4db460301..36defa117 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java @@ -5,21 +5,31 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import groovy.lang.GroovyClassLoader; import org.apache.commons.lang3.builder.ToStringBuilder; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Map; class CompiledScript extends CompiledClass { + public static String classNameFromPath(String path) { + int i = path.lastIndexOf('.'); + path = path.substring(0, i); + return path.replace('/', '.'); + } + final List innerClasses = new ArrayList<>(); long lastEdited; List preprocessors; + private boolean preprocessorCheckFailed; + private boolean requiresReload; public CompiledScript(String path, long lastEdited) { - this(path, null, lastEdited); + this(path, classNameFromPath(path), lastEdited); } public CompiledScript(String path, String name, long lastEdited) { @@ -31,6 +41,12 @@ public boolean isClosure() { return lastEdited < 0; } + @Override + public void onCompile(Class clazz, String basePath) { + setRequiresReload(this.data == null); + super.onCompile(clazz, basePath); + } + public CompiledClass findInnerClass(String clazz) { for (CompiledClass comp : this.innerClasses) { if (comp.name.equals(clazz)) { @@ -42,17 +58,17 @@ public CompiledClass findInnerClass(String clazz) { return comp; } - public void ensureLoaded(CachedClassLoader classLoader, String basePath) { + public void ensureLoaded(GroovyClassLoader classLoader, Map cache, String basePath) { for (CompiledClass comp : this.innerClasses) { if (comp.clazz == null) { if (comp.readData(basePath)) { - comp.ensureLoaded(classLoader, basePath); + comp.ensureLoaded(classLoader, cache, basePath); } else { GroovyLog.get().error("Error loading inner class {} for class {}", comp.name, this.name); } } } - super.ensureLoaded(classLoader, basePath); + super.ensureLoaded(classLoader, cache, basePath); } public @NotNull JsonObject toJson() { @@ -109,10 +125,25 @@ public void deleteCache(String cachePath) { } } - public boolean checkPreprocessors(File basePath) { - return this.preprocessors == null || this.preprocessors.isEmpty() || Preprocessor.validatePreprocessor( - new File(basePath, this.path), - this.preprocessors); + public boolean checkPreprocessorsFailed(File basePath) { + setPreprocessorCheckFailed(this.preprocessors != null && !this.preprocessors.isEmpty() && !Preprocessor.validatePreprocessor(new File(basePath, this.path), this.preprocessors)); + return preprocessorCheckFailed(); + } + + public boolean requiresReload() { + return this.requiresReload; + } + + public boolean preprocessorCheckFailed() { + return this.preprocessorCheckFailed; + } + + protected void setRequiresReload(boolean requiresReload) { + this.requiresReload = requiresReload; + } + + protected void setPreprocessorCheckFailed(boolean preprocessorCheckFailed) { + this.preprocessorCheckFailed = preprocessorCheckFailed; } @Override diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java new file mode 100644 index 000000000..701be4738 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java @@ -0,0 +1,569 @@ +package com.cleanroommc.groovyscript.sandbox; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.helper.JsonHelper; +import com.google.common.collect.AbstractIterator; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import groovy.lang.GroovyCodeSource; +import groovy.util.ResourceConnector; +import groovy.util.ResourceException; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.apache.commons.io.FileUtils; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.*; +import org.codehaus.groovy.runtime.IOGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.tools.gse.DependencyTracker; +import org.codehaus.groovy.tools.gse.StringSetMap; +import org.codehaus.groovy.vmplugin.VMPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.security.CodeSource; +import java.util.*; + +public class CustomGroovyScriptEngine implements ResourceConnector { + + /** + * Changing this number will force the cache to be deleted and every script has to be recompiled. + * Useful when changes to the compilation process were made. + */ + public static final int CACHE_VERSION = 4; + /** + * Setting this to false will cause compiled classes to never be cached. + * As a side effect some compilation behaviour might change. Can be useful for debugging. + */ + public static final boolean ENABLE_CACHE = true; + /** + * Setting this to true will cause the cache to be deleted before each script run. + * Useful for debugging. + */ + public static final boolean DELETE_CACHE_ON_RUN = Boolean.parseBoolean(System.getProperty("groovyscript.disable_cache")); + + private static WeakReference> localData = new WeakReference<>(null); + + private static synchronized ThreadLocal getLocalData() { + ThreadLocal local = localData.get(); + if (local != null) return local; + local = new ThreadLocal<>(); + localData = new WeakReference<>(local); + return local; + } + + private final URL[] scriptEnvironment; + private final File cacheRoot; + private final File scriptRoot; + private final CompilerConfiguration config; + private final ScriptClassLoader classLoader; + private final Map index = new Object2ObjectOpenHashMap<>(); + private final Map loadedClasses = new Object2ObjectOpenHashMap<>(); + + public CustomGroovyScriptEngine(URL[] scriptEnvironment, File cacheRoot, File scriptRoot, CompilerConfiguration config) { + this.scriptEnvironment = scriptEnvironment; + this.cacheRoot = cacheRoot; + this.scriptRoot = scriptRoot; + this.config = config; + this.classLoader = new ScriptClassLoader(CustomGroovyScriptEngine.class.getClassLoader(), config, Collections.unmodifiableMap(this.loadedClasses)); + readIndex(); + } + + public File getScriptRoot() { + return scriptRoot; + } + + public File getCacheRoot() { + return cacheRoot; + } + + public CompilerConfiguration getConfig() { + return config; + } + + public GroovyScriptClassLoader getClassLoader() { + return classLoader; + } + + public Iterable> getAllLoadedScriptClasses() { + return () -> new AbstractIterator<>() { + + private final Iterator it = loadedClasses.values().iterator(); + private Iterator innerClassesIt; + + @Override + protected Class computeNext() { + if (innerClassesIt != null && innerClassesIt.hasNext()) { + return innerClassesIt.next().clazz; + } + innerClassesIt = null; + CompiledClass cc; + while (it.hasNext()) { + cc = it.next(); + if (cc instanceof CompiledScript cs && !cs.preprocessorCheckFailed() && cs.clazz != null) { + if (!cs.innerClasses.isEmpty()) { + innerClassesIt = cs.innerClasses.iterator(); + } + return cs.clazz; + } + } + return endOfData(); + } + }; + } + + void readIndex() { + this.index.clear(); + JsonElement jsonElement = JsonHelper.loadJson(new File(this.cacheRoot, "_index.json")); + if (jsonElement == null || !jsonElement.isJsonObject()) return; + JsonObject json = jsonElement.getAsJsonObject(); + int cacheVersion = json.get("version").getAsInt(); + String java = json.has("java") ? json.get("java").getAsString() : ""; + if (cacheVersion != CACHE_VERSION || !java.equals(VMPlugin.getJavaVersion())) { + // cache version changed -> force delete cache + deleteScriptCache(); + return; + } + for (JsonElement element : json.getAsJsonArray("index")) { + if (element.isJsonObject()) { + CompiledScript cs = CompiledScript.fromJson(element.getAsJsonObject(), this.scriptRoot.getPath(), this.cacheRoot.getPath()); + if (cs != null) { + this.index.put(cs.path, cs); + this.loadedClasses.put(cs.name, cs); + for (CompiledClass cc : cs.innerClasses) { + this.loadedClasses.put(cc.name, cc); + } + } + } + } + } + + void writeIndex() { + if (!ENABLE_CACHE) return; + JsonObject json = new JsonObject(); + json.addProperty("!DANGER!", "DO NOT EDIT THIS FILE!!!"); + json.addProperty("version", CACHE_VERSION); + json.addProperty("java", VMPlugin.getJavaVersion()); + JsonArray index = new JsonArray(); + json.add("index", index); + for (Map.Entry entry : this.index.entrySet()) { + index.add(entry.getValue().toJson()); + } + JsonHelper.saveJson(new File(this.cacheRoot, "_index.json"), json); + } + + @ApiStatus.Internal + public boolean deleteScriptCache() { + this.index.values().forEach(script -> script.deleteCache(this.cacheRoot.getPath())); + this.index.clear(); + this.loadedClasses.clear(); + getClassLoader().clearCache(); + try { + FileUtils.cleanDirectory(this.cacheRoot); + return true; + } catch (IOException e) { + GroovyScript.LOGGER.throwing(e); + return false; + } + } + + List findScripts(Collection files) { + List scripts = new ArrayList<>(files.size()); + for (File file : files) { + CompiledScript cs = checkScriptLoadability(file); + if (!cs.preprocessorCheckFailed()) scripts.add(cs); + } + return scripts; + } + + void loadScript(CompiledScript script) { + if (script.requiresReload() && !script.preprocessorCheckFailed()) { + Class clazz = loadScriptClassInternal(new File(script.path), true); + script.setRequiresReload(false); + if (script.clazz == null) { + // should not happen + GroovyLog.get().errorMC("Class for {} was loaded, but didn't receive class created callback!", script.path); + if (ENABLE_CACHE) script.clazz = clazz; + } + } + } + + CompiledScript loadScriptClass(File file) { + CompiledScript compiledScript = checkScriptLoadability(file); + loadScript(compiledScript); + return compiledScript; + } + + @NotNull + CompiledScript checkScriptLoadability(File file) { + String relativeFileName = FileUtil.relativize(this.scriptRoot.getPath(), file.getPath()); + File relativeFile = new File(relativeFileName); + long lastModified = file.lastModified(); + CompiledScript comp = this.index.get(relativeFileName); + + if (ENABLE_CACHE && comp != null && lastModified <= comp.lastEdited && comp.clazz == null && comp.readData(this.cacheRoot.getPath())) { + // class is not loaded, but the cached class bytes are still valid + comp.setRequiresReload(false); + if (comp.checkPreprocessorsFailed(this.scriptRoot)) { + return comp; + } + comp.ensureLoaded(getClassLoader(), this.loadedClasses, this.cacheRoot.getPath()); + } else if (!ENABLE_CACHE || (comp == null || comp.clazz == null || lastModified > comp.lastEdited)) { + // class is not loaded and class bytes don't exist yet or script has been edited + if (comp == null) { + comp = new CompiledScript(relativeFileName, 0); + this.index.put(relativeFileName, comp); + } + if (comp.clazz != null) { + InvokerHelper.removeClass(comp.clazz); + comp.clazz = null; + } + comp.setRequiresReload(true); + if (lastModified > comp.lastEdited || comp.preprocessors == null) { + // recompile preprocessors if there is no data or script was edited + comp.preprocessors = Preprocessor.parsePreprocessors(file); + } + comp.lastEdited = lastModified; + if (comp.checkPreprocessorsFailed(this.scriptRoot)) { + // delete class bytes to make sure it's recompiled once the preprocessors returns true + comp.deleteCache(this.cacheRoot.getPath()); + return comp; + } + } else { + // class is loaded and script wasn't edited + comp.setRequiresReload(false); + if (comp.checkPreprocessorsFailed(this.scriptRoot)) { + return comp; + } + comp.ensureLoaded(getClassLoader(), this.loadedClasses, this.cacheRoot.getPath()); + } + comp.setPreprocessorCheckFailed(false); + return comp; + } + + protected Class loadScriptClassInternal(File file, boolean isFileRelative) { + Class scriptClass = null; + try { + scriptClass = parseDynamicScript(file, isFileRelative); + } catch (Exception e) { + GroovyLog.get().exception("An error occurred while trying to load script class " + file.toString(), e); + } + return scriptClass; + } + + @Nullable + private File findScriptFileOfClass(String className) { + for (String ending : this.config.getScriptExtensions()) { + File file = findScriptFile(className + "." + ending); + if (file != null) return file; + } + return null; + } + + @Nullable + private File findScriptFile(String scriptName) { + File file; + for (URL root : this.scriptEnvironment) { + try { + File rootFile = new File(root.toURI()); + // try to combine the root with the file ending + file = new File(rootFile, scriptName); + if (file.exists()) { + // found a valid file + return file; + } + } catch (URISyntaxException e) { + GroovyScript.LOGGER.throwing(e); + } + } + return null; + } + + private @Nullable Class parseDynamicScript(File file, boolean isFileRelative) { + if (isFileRelative) { + file = findScriptFile(file.getPath()); + if (file == null) return null; + } + Class clazz = null; + try { + String encoding = config.getSourceEncoding(); + String content = IOGroovyMethods.getText(new FileInputStream(file), encoding); + clazz = this.classLoader.parseClassRaw(content, file.toPath().toUri().toURL().toExternalForm()); + // manually load the file as a groovy script + //clazz = this.classLoader.parseClassRaw(path.toFile()); + } catch (IOException e) { + GroovyScript.LOGGER.throwing(e); + } + return clazz; + } + + /** + * Called via mixin when groovy compiled a class from scripts. + */ + @ApiStatus.Internal + public void onCompileClass(SourceUnit su, String path, Class clazz, byte[] code, boolean inner) { + String shortPath = FileUtil.relativize(this.scriptRoot.getPath(), path); + // if the script was compiled because another script depends on it, the source unit is wrong + // we need to find the source unit of the compiled class + SourceUnit trueSource = su.getAST().getUnit().getScriptSourceLocation(mainClassName(clazz.getName())); + String truePath = trueSource == null ? shortPath : FileUtil.relativize(this.scriptRoot.getPath(), trueSource.getName()); + if (shortPath.equals(truePath) && su.getAST().getMainClassName() != null && !su.getAST().getMainClassName().equals(clazz.getName())) { + inner = true; + } + + boolean finalInner = inner; + CompiledScript comp = this.index.computeIfAbsent(truePath, k -> new CompiledScript(k, finalInner ? -1 : 0)); + CompiledClass innerClass = comp; + if (inner) innerClass = comp.findInnerClass(clazz.getName()); + innerClass.onCompile(code, clazz, this.cacheRoot.getPath()); + this.loadedClasses.put(innerClass.name, innerClass); + } + + /** + * Called via mixin when a script class needs to be recompiled. This happens when a script was loaded because another script depends on + * it. Groovy will then try to compile the script again. If we already compiled the class we just stop the compilation process. + */ + @ApiStatus.Internal + public Class onRecompileClass(URL source, String className) { + String path = source.toExternalForm(); + String rel = FileUtil.relativize(this.scriptRoot.getPath(), path); + CompiledScript cs = this.index.get(rel); + Class c = null; + if (cs != null) { + if (cs.clazz == null && cs.readData(this.cacheRoot.getPath())) { + cs.ensureLoaded(getClassLoader(), this.loadedClasses, this.cacheRoot.getPath()); + } + c = cs.clazz; + } + return c; + } + + private static String mainClassName(String name) { + return name.contains("$") ? name.split("\\$", 2)[0] : name; + } + + + @Override + public URLConnection getResourceConnection(String resourceName) throws ResourceException { + // Get the URLConnection + URLConnection groovyScriptConn = null; + + ResourceException se = null; + for (URL root : this.scriptEnvironment) { + URL scriptURL = null; + try { + scriptURL = new URL(root, resourceName); + groovyScriptConn = openConnection(scriptURL); + + break; // Now this is a bit unusual + } catch (MalformedURLException e) { + String message = "Malformed URL: with context=" + root + " and spec=" + resourceName + " because " + e.getMessage(); + if (se == null) { + se = new ResourceException(message); + } else { + se = new ResourceException(message, se); + } + } catch (IOException e1) { + String message = "Cannot open URL: " + scriptURL; + groovyScriptConn = null; + if (se == null) { + se = new ResourceException(message); + } else { + se = new ResourceException(message, se); + } + } + } + + if (se == null) se = new ResourceException("No resource for " + resourceName + " was found"); + + // If we didn't find anything, report on all the exceptions that occurred. + if (groovyScriptConn == null) throw se; + return groovyScriptConn; + } + + private static URLConnection openConnection(URL scriptURL) throws IOException { + URLConnection urlConnection = scriptURL.openConnection(); + verifyInputStream(urlConnection); + + return scriptURL.openConnection(); + } + + private static void forceClose(URLConnection urlConnection) { + if (urlConnection != null) { + // We need to get the input stream and close it to force the open + // file descriptor to be released. Otherwise, we will reach the limit + // for number of files open at one time. + + try { + verifyInputStream(urlConnection); + } catch (Exception e) { + // Do nothing: We were not going to use it anyway. + } + } + } + + private static void verifyInputStream(URLConnection urlConnection) throws IOException { + try (InputStream in = urlConnection.getInputStream()) { + } + } + + private static class LocalData { + + CompilationUnit cu; + final StringSetMap dependencyCache = new StringSetMap(); + final Map precompiledEntries = new HashMap<>(); + } + + private class ScriptClassLoader extends GroovyScriptClassLoader { + + public ScriptClassLoader(ClassLoader loader, CompilerConfiguration config, Map cache) { + super(loader, config, cache); + init(); + } + + @Override + public URL loadResource(String name) throws MalformedURLException { + File file = CustomGroovyScriptEngine.this.findScriptFileOfClass(name); + if (file != null) { + return file.toURI().toURL(); + } + return null; + } + + @Override + protected ClassCollector createCustomCollector(CompilationUnit unit, SourceUnit su) { + return super.createCustomCollector(unit, su).creatClassCallback((code, clz) -> { + onCompileClass(su, su.getName(), clz, code, clz.getName().contains("$")); + }); + } + + @Override + protected CompilationUnit createCompilationUnit(CompilerConfiguration configuration, CodeSource source) { + CompilationUnit cu = super.createCompilationUnit(configuration, source); + LocalData local = getLocalData().get(); + local.cu = cu; + final StringSetMap cache = local.dependencyCache; + final Map precompiledEntries = local.precompiledEntries; + + // "." is used to transfer compilation dependencies, which will be + // recollected later during compilation + for (String depSourcePath : cache.get(".")) { + try { + cache.get(depSourcePath); + cu.addSource(getResourceConnection(depSourcePath).getURL()); // todo remove usage of resource connection + } catch (ResourceException e) { + /* ignore */ + } + } + + // remove all old entries including the "." entry + cache.clear(); + + cu.addPhaseOperation((final SourceUnit sourceUnit, final GeneratorContext context, final ClassNode classNode) -> { + // GROOVY-4013: If it is an inner class, tracking its dependencies doesn't really + // serve any purpose and also interferes with the caching done to track dependencies + if (classNode.getOuterClass() != null) return; + DependencyTracker dt = new DependencyTracker(sourceUnit, cache, precompiledEntries); + dt.visitClass(classNode); + }, Phases.CLASS_GENERATION); + + cu.setClassNodeResolver(new ClassNodeResolver() { + + @Override + public LookupResult findClassNode(String origName, CompilationUnit compilationUnit) { + String name = origName.replace('.', '/'); + File scriptFile = CustomGroovyScriptEngine.this.findScriptFileOfClass(name); + if (scriptFile != null) { + CompiledScript result = checkScriptLoadability(scriptFile); + if (result.requiresReload() || result.clazz == null) { + try { + return new LookupResult(compilationUnit.addSource(scriptFile.toURI().toURL()), null); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } else { + return new LookupResult(null, ClassHelper.make(result.clazz)); + } + } + return super.findClassNode(origName, compilationUnit); + } + }); + + return cu; + } + + @Override + protected Class recompile(URL source, String className) throws CompilationFailedException, IOException { + if (source != null) { + Class c = CustomGroovyScriptEngine.this.onRecompileClass(source, className); + if (c != null) { + return c; + } + } + return super.recompile(source, className); + } + + @Override + public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException { + synchronized (sourceCache) { + File file = codeSource.getFile(); + if (file == null) { + file = new File(codeSource.getName()); + } + CompiledScript compiledScript = loadScriptClass(file); + if (compiledScript.preprocessorCheckFailed()) { + throw new IllegalStateException("Figure this out"); + } + if (compiledScript.requiresReload() || compiledScript.clazz == null) { + compiledScript.clazz = CustomGroovyScriptEngine.this.parseDynamicScript(file, false); + } + return compiledScript.clazz; + } + } + + public Class parseClassRaw(GroovyCodeSource source) { + synchronized (sourceCache) { + return doParseClass(source); + } + } + + public Class parseClassRaw(File file) throws IOException { + return parseClassRaw(new GroovyCodeSource(file, CustomGroovyScriptEngine.this.config.getSourceEncoding())); + } + + public Class parseClassRaw(final String text, final String fileName) throws CompilationFailedException { + GroovyCodeSource gcs = new GroovyCodeSource(text, fileName, "/groovy/script"); + gcs.setCachable(false); + return parseClassRaw(gcs); + } + + private Class doParseClass(GroovyCodeSource codeSource) { + // local is kept as hard reference to avoid garbage collection + ThreadLocal localTh = getLocalData(); + LocalData localData = new LocalData(); + localTh.set(localData); + StringSetMap cache = localData.dependencyCache; + Class answer = null; + try { + answer = super.parseClass(codeSource, false); + } finally { + cache.clear(); + localTh.remove(); + } + return answer; + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java index cf38f96e3..020e1d4d7 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java @@ -283,7 +283,7 @@ public void exception(String msg, Throwable throwable) { private List prepareStackTrace(StackTraceElement[] stackTrace) { List lines = Arrays.stream(stackTrace).map(StackTraceElement::toString).collect(Collectors.toList()); - String engineCause = "com.cleanroommc.groovyscript.sandbox.GroovySandbox.loadScripts"; + String engineCause = "com.cleanroommc.groovyscript.sandbox.GroovyScriptSandbox.loadScripts"; int i = 0; for (String line : lines) { i++; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java deleted file mode 100644 index 886d224eb..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.cleanroommc.groovyscript.sandbox; - -import com.cleanroommc.groovyscript.GroovyScript; -import com.cleanroommc.groovyscript.api.GroovyLog; -import com.cleanroommc.groovyscript.api.INamed; -import com.cleanroommc.groovyscript.helper.Alias; -import groovy.lang.Binding; -import groovy.lang.Closure; -import groovy.lang.Script; -import groovy.util.GroovyScriptEngine; -import groovy.util.ResourceException; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.customizers.ImportCustomizer; -import org.codehaus.groovy.runtime.InvokerHelper; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; - -/** - * @author brachy84 - */ -public abstract class GroovySandbox { - - private String currentScript; - - private final URL[] scriptEnvironment; - private final ThreadLocal running = ThreadLocal.withInitial(() -> false); - private final Map bindings = new Object2ObjectOpenHashMap<>(); - private final ImportCustomizer importCustomizer = new ImportCustomizer(); - private final CachedClassLoader ccl = new CachedClassLoader(); - - protected GroovySandbox(URL[] scriptEnvironment) { - if (scriptEnvironment == null || scriptEnvironment.length == 0) { - throw new NullPointerException("Script Environment must be non null and at least contain one URL!"); - } - this.scriptEnvironment = scriptEnvironment; - } - - protected GroovySandbox(List scriptEnvironment) { - this(scriptEnvironment.toArray(new URL[0])); - } - - public void registerBinding(String name, Object obj) { - Objects.requireNonNull(name); - Objects.requireNonNull(obj); - for (String alias : Alias.generateOf(name)) { - bindings.put(alias, obj); - } - } - - public void registerBinding(INamed named) { - Objects.requireNonNull(named); - for (String alias : named.getAliases()) { - bindings.put(alias, named); - } - } - - protected void startRunning() { - this.running.set(true); - } - - protected void stopRunning() { - this.running.set(false); - } - - protected GroovyScriptEngine createScriptEngine() { - GroovyScriptEngine engine = new GroovyScriptEngine(this.scriptEnvironment, this.ccl); - CompilerConfiguration config = new CompilerConfiguration(CompilerConfiguration.DEFAULT); - config.setSourceEncoding("UTF-8"); - config.addCompilationCustomizers(this.importCustomizer); - engine.setConfig(config); - initEngine(engine, config); - return engine; - } - - protected Binding createBindings() { - Binding binding = new Binding(this.bindings); - postInitBindings(binding); - return binding; - } - - public void load() throws Exception { - preRun(); - - GroovyScriptEngine engine = createScriptEngine(); - Binding binding = createBindings(); - Set executedClasses = new ObjectOpenHashSet<>(); - - this.running.set(true); - try { - load(engine, binding, executedClasses, true); - } finally { - this.running.set(false); - postRun(); - setCurrentScript(null); - } - } - - protected void load(GroovyScriptEngine engine, Binding binding, Set executedClasses, boolean run) { - // load and run any configured class files - loadClassScripts(engine, binding, executedClasses, run); - // now run all script files - loadScripts(engine, binding, executedClasses, run); - } - - protected void loadScripts(GroovyScriptEngine engine, Binding binding, Set executedClasses, boolean run) { - for (File scriptFile : getScriptFiles()) { - if (!executedClasses.contains(scriptFile)) { - Class clazz = loadScriptClass(engine, scriptFile); - if (clazz == GroovyLog.class) continue; // preprocessor returned false - if (clazz == null) { - GroovyLog.get().errorMC("Error loading script for {}", scriptFile.getPath()); - GroovyLog.get().errorMC("Did you forget to register your class file in your run config?"); - continue; - } - if (clazz.getSuperclass() != Script.class) { - GroovyLog.get().errorMC("Class file '{}' should be defined in the runConfig in the classes property!", scriptFile); - continue; - } - if (shouldRunFile(scriptFile)) { - Script script = InvokerHelper.createScript(clazz, binding); - if (run) runScript(script); - } - } - } - } - - protected void loadClassScripts(GroovyScriptEngine engine, Binding binding, Set executedClasses, boolean run) { - for (File classFile : getClassFiles()) { - if (executedClasses.contains(classFile)) continue; - Class clazz = loadScriptClass(engine, classFile); - if (clazz == GroovyLog.class) continue; // preprocessor returned false - if (clazz == null) { - // loading script fails if the file is a script that depends on a class file that isn't loaded yet - // we cant determine if the file is a script or a class - continue; - } - // the superclass of class files is Object - if (clazz.getSuperclass() != Script.class && shouldRunFile(classFile)) { - try { - // $getLookup is present on all groovy created classes - // call it cause the class to be initialised - Method m = clazz.getMethod("$getLookup"); - m.invoke(null); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - GroovyLog.get().errorMC("Error initialising class '{}'", clazz.getName()); - } - executedClasses.add(classFile); - } - } - } - - protected void runScript(Script script) { - setCurrentScript(script.getClass().getName()); - script.run(); - setCurrentScript(null); - } - - public T runClosure(Closure closure, Object... args) { - boolean wasRunning = isRunning(); - if (!wasRunning) startRunning(); - T result = null; - try { - result = closure.call(args); - } catch (Exception e) { - GroovyScript.LOGGER.error("Caught an exception trying to run a closure:"); - e.printStackTrace(); - } finally { - if (!wasRunning) stopRunning(); - } - return result; - } - - @ApiStatus.OverrideOnly - protected void postInitBindings(Binding binding) {} - - @ApiStatus.OverrideOnly - protected void initEngine(GroovyScriptEngine engine, CompilerConfiguration config) {} - - @ApiStatus.OverrideOnly - protected void preRun() {} - - @ApiStatus.OverrideOnly - protected boolean shouldRunFile(File file) { - return true; - } - - @ApiStatus.OverrideOnly - protected void postRun() {} - - public abstract Collection getClassFiles(); - - public abstract Collection getScriptFiles(); - - public boolean isRunning() { - return this.running.get(); - } - - public Map getBindings() { - return bindings; - } - - public ImportCustomizer getImportCustomizer() { - return importCustomizer; - } - - public String getCurrentScript() { - return currentScript; - } - - protected void setCurrentScript(String currentScript) { - this.currentScript = currentScript; - } - - public CachedClassLoader getClassLoader() { - return ccl; - } - - public static String getRelativePath(String source) { - try { - Path path = Paths.get(new URL(source).toURI()); - Path mainPath = new File(GroovyScript.getScriptPath()).toPath(); - return mainPath.relativize(path).toString(); - } catch (URISyntaxException | MalformedURLException e) { - GroovyScript.LOGGER.error("Error parsing script source '{}'", source); - // don't log to GroovyLog here since it will cause a StackOverflow - return source; - } - } - - protected Class loadScriptClass(GroovyScriptEngine engine, File file) { - Class scriptClass = null; - try { - try { - // this will only work for files that existed when the game launches - scriptClass = engine.loadScriptByName(file.toString()); - // extra safety - if (scriptClass == null) { - scriptClass = tryLoadDynamicFile(engine, file); - } - } catch (ResourceException e) { - // file was added later, causing a ResourceException - // try to manually load the file - scriptClass = tryLoadDynamicFile(engine, file); - } - - // if the file is still not found something went wrong - } catch (Exception e) { - GroovyLog.get().exception("An error occurred while trying to load script class " + file.toString(), e); - } - return scriptClass; - } - - private @Nullable Class tryLoadDynamicFile(GroovyScriptEngine engine, File file) throws ResourceException { - Path path = null; - for (URL root : this.scriptEnvironment) { - try { - File rootFile = new File(root.toURI()); - // try to combine the root with the file ending - path = new File(rootFile, file.toString()).toPath(); - if (Files.exists(path)) { - // found a valid file - break; - } - } catch (URISyntaxException e) { - e.printStackTrace(); - } - } - - if (path == null) return null; - - GroovyLog.get().debugMC("Found path '{}' for dynamic file {}", path, file.toString()); - - Class clazz = null; - try { - // manually load the file as a groovy script - clazz = engine.getGroovyClassLoader().parseClass(path.toFile()); - } catch (IOException e) { - e.printStackTrace(); - } - return clazz; - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java new file mode 100644 index 000000000..fce4c9fb8 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java @@ -0,0 +1,534 @@ +package com.cleanroommc.groovyscript.sandbox; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.GroovyCodeSource; +import groovy.lang.GroovyResourceLoader; +import groovy.util.CharsetToolkit; +import groovyjarjarasm.asm.ClassVisitor; +import groovyjarjarasm.asm.ClassWriter; +import net.minecraft.launchwrapper.Launch; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.control.*; +import org.codehaus.groovy.util.URLStreams; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.CodeSource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Map; +import java.util.function.BiConsumer; + +public abstract class GroovyScriptClassLoader extends GroovyClassLoader { + + private final CompilerConfiguration config; + private final String sourceEncoding; + + private final Map cache; + + private GroovyScriptClassLoader(GroovyScriptClassLoader parent) { + this(parent, parent.config, parent.cache); + } + + GroovyScriptClassLoader(ClassLoader parent, CompilerConfiguration config, Map cache) { + super(parent, config, false); + this.config = config; + this.sourceEncoding = initSourceEncoding(config); + this.cache = cache; + } + + protected void init() { + setResourceLoader(this::loadResource); + setShouldRecompile(false); + } + + private String initSourceEncoding(CompilerConfiguration config) { + String sourceEncoding = config.getSourceEncoding(); + if (null == sourceEncoding) { + // Keep the same default source encoding with the one used by #parseClass(InputStream, String) + // TODO should we use org.codehaus.groovy.control.CompilerConfiguration.DEFAULT_SOURCE_ENCODING instead? + return CharsetToolkit.getDefaultSystemCharset().name(); + } + return sourceEncoding; + } + + public abstract @Nullable URL loadResource(String name) throws MalformedURLException; + + @Override + protected void setClassCacheEntry(Class cls) {} + + @Override + protected Class getClassCacheEntry(String name) { + CompiledClass cc = this.cache.get(name); + return cc != null ? cc.clazz : null; + } + + @Override + public Class defineClass(ClassNode classNode, String file, String newCodeBase) { + CodeSource codeSource = null; + try { + codeSource = new CodeSource(new URL("file", "", newCodeBase), (java.security.cert.Certificate[]) null); + } catch (MalformedURLException e) { + //swallow + } + + CompilationUnit unit = createCompilationUnit(config, codeSource); + ClassCollector collector = createCustomCollector(unit, classNode.getModule().getContext()); + try { + unit.addClassNode(classNode); + unit.setClassgenCallback(collector); + unit.compile(Phases.CLASS_GENERATION); + definePackageInternal(collector.generatedClass.getName()); + return collector.generatedClass; + } catch (CompilationFailedException e) { + throw new RuntimeException(e); + } + } + + /** + * Parses the given code source into a Java class. If there is a class file + * for the given code source, then no parsing is done, instead the cached class is returned. + * + * @param shouldCacheSource if true then the generated class will be stored in the source cache + * @return the main class defined in the given script + */ + @Override + public Class parseClass(final GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException { + return doParseClass(codeSource); + } + + private Class doParseClass(GroovyCodeSource codeSource) { + validate(codeSource); + Class answer; // Was neither already loaded nor compiling, so compile and add to cache. + CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource()); + /*if (recompile!=null && recompile || recompile==null && config.getRecompileGroovySource()) { + unit.addFirstPhaseOperation(GroovyClassLoader.TimestampAdder.INSTANCE, CompilePhase.CLASS_GENERATION.getPhaseNumber()); + }*/ + SourceUnit su = null; + File file = codeSource.getFile(); + if (file != null) { + su = unit.addSource(file); + } else { + URL url = codeSource.getURL(); + if (url != null) { + su = unit.addSource(url); + } else { + su = unit.addSource(codeSource.getName(), codeSource.getScriptText()); + } + } + + ClassCollector collector = createCustomCollector(unit, su); + unit.setClassgenCallback(collector); + int goalPhase = Phases.CLASS_GENERATION; + if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT; + unit.compile(goalPhase); + + answer = collector.generatedClass; + String mainClass = su.getAST().getMainClassName(); + for (Class clazz : collector.getLoadedClasses()) { + String clazzName = clazz.getName(); + definePackageInternal(clazzName); + setClassCacheEntry(clazz); + if (clazzName.equals(mainClass)) answer = clazz; + } + return answer; + } + + /** + * loads a class from a file or a parent classloader. + * + * @param name of the class to be loaded + * @param lookupScriptFiles if false no lookup at files is done at all + * @param preferClassOverScript if true the file lookup is only done if there is no class + * @param resolve see {@link java.lang.ClassLoader#loadClass(java.lang.String, boolean)} + * @return the class found or the class created from a file lookup + * @throws ClassNotFoundException if the class could not be found + * @throws CompilationFailedException if the source file could not be compiled + */ + @Override + public Class loadClass(final String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException { + // look into cache + Class cls = getClassCacheEntry(name); + + // enable recompilation? + boolean recompile = isRecompilable(cls); + if (!recompile) return cls; + + // try parent loader + ClassNotFoundException last = null; + try { + Class parentClassLoaderClass = Launch.classLoader.findClass(name);//super.loadClass(name, resolve); + // always return if the parent loader was successful + if (parentClassLoaderClass != null) return parentClassLoaderClass; + } catch (ClassNotFoundException cnfe) { + last = cnfe; + } catch (NoClassDefFoundError ncdfe) { + if (ncdfe.getMessage().indexOf("wrong name") > 0) { + last = new ClassNotFoundException(name); + } else { + throw ncdfe; + } + } + + // at this point the loading from a parent loader failed, + // and we want to recompile if needed. + if (lookupScriptFiles) { + // try groovy file + try { + // check if recompilation already happened. + final Class classCacheEntry = getClassCacheEntry(name); + if (classCacheEntry != cls) return classCacheEntry; + URL source = loadResource(name); + // if recompilation fails, we want cls==null + cls = recompile(source, name); + } catch (IOException ioe) { + last = new ClassNotFoundException("IOException while opening groovy source: " + name, ioe); + } finally { + if (cls == null) { + removeClassCacheEntry(name); + } else { + setClassCacheEntry(cls); + } + } + } + + if (cls == null) { + // no class found, there should have been an exception before now + if (last == null) throw new AssertionError(true); + throw last; + } + return cls; + } + + @Override + protected Class recompile(URL source, String className, Class oldClass) throws CompilationFailedException, IOException { + throw new UnsupportedOperationException(); + } + + /** + * (Re)Compiles the given source. + * This method starts the compilation of a given source, if + * the source has changed since the class was created. For + * this isSourceNewer is called. + * + * @param source the source pointer for the compilation + * @param className the name of the class to be generated + * @return the old class if the source wasn't new enough, the new class else + * @throws CompilationFailedException if the compilation failed + * @throws IOException if the source is not readable + * @see #isSourceNewer(URL, Class) + */ + protected Class recompile(URL source, String className) throws CompilationFailedException, IOException { + if (source != null) { + String name = source.toExternalForm(); + if (isFile(source)) { + try { + return parseClass(new GroovyCodeSource(new File(source.toURI()), sourceEncoding)); + } catch (URISyntaxException e) { + // do nothing and fall back to the other version + } + } + return parseClass(new InputStreamReader(URLStreams.openUncachedStream(source), sourceEncoding), name); + } + return null; + } + + /** + * gets the time stamp of a given class. For groovy + * generated classes this usually means to return the value + * of the static field __timeStamp. If the parameter doesn't + * have such a field, then Long.MAX_VALUE is returned + * + * @param cls the class + * @return the time stamp + */ + @Override + protected long getTimeStamp(Class cls) { + return Long.MAX_VALUE; + } + + private static boolean isFile(URL ret) { + return ret != null && ret.getProtocol().equals("file"); + } + + private static void validate(GroovyCodeSource codeSource) { + if (codeSource.getFile() == null && codeSource.getScriptText() == null) { + throw new IllegalArgumentException("Script text to compile cannot be null!"); + } + } + + @SuppressWarnings("deprecation") // TODO replace getPackage with getDefinedPackage once min JDK version >= 9 + private void definePackageInternal(String className) { + int i = className.lastIndexOf('.'); + if (i != -1) { + String pkgName = className.substring(0, i); + java.lang.Package pkg = getPackage(pkgName); + if (pkg == null) { + definePackage(pkgName, null, null, null, null, null, null, null); + } + } + } + + /** + * creates a ClassCollector for a new compilation. + * + * @param unit the compilationUnit + * @param su the SourceUnit + * @return the ClassCollector + */ + protected ClassCollector createCustomCollector(CompilationUnit unit, SourceUnit su) { + return new ClassCollector(new InnerLoader(this), unit, su); + } + + @Override + protected GroovyClassLoader.ClassCollector createCollector(CompilationUnit unit, SourceUnit su) { + throw new UnsupportedOperationException(); + } + + public static class ClassCollector implements CompilationUnit.ClassgenCallback { + + private Class generatedClass; + private final GroovyScriptClassLoader cl; + private final SourceUnit su; + private final CompilationUnit unit; + private final Collection> loadedClasses; + private BiConsumer> creatClassCallback; + + protected ClassCollector(GroovyScriptClassLoader cl, CompilationUnit unit, SourceUnit su) { + this.cl = cl; + this.unit = unit; + this.loadedClasses = new ArrayList<>(); + this.su = su; + } + + public GroovyScriptClassLoader getDefiningClassLoader() { + return cl; + } + + protected Class createClass(byte[] code, ClassNode classNode) { + BytecodeProcessor bytecodePostprocessor = unit.getConfiguration().getBytecodePostprocessor(); + byte[] fcode = code; + if (bytecodePostprocessor != null) { + fcode = bytecodePostprocessor.processBytecode(classNode.getName(), fcode); + } + GroovyScriptClassLoader cl = getDefiningClassLoader(); + Class theClass = cl.defineClass(classNode.getName(), fcode, 0, fcode.length, unit.getAST().getCodeSource()); + this.loadedClasses.add(theClass); + + if (generatedClass == null) { + ModuleNode mn = classNode.getModule(); + SourceUnit msu = null; + if (mn != null) msu = mn.getContext(); + ClassNode main = null; + if (mn != null) main = mn.getClasses().get(0); + if (msu == su && main == classNode) generatedClass = theClass; + } + + if (this.creatClassCallback != null) { + this.creatClassCallback.accept(code, theClass); + } + return theClass; + } + + protected Class onClassNode(ClassWriter classWriter, ClassNode classNode) { + byte[] code = classWriter.toByteArray(); + return createClass(code, classNode); + } + + @Override + public void call(ClassVisitor classWriter, ClassNode classNode) { + onClassNode((ClassWriter) classWriter, classNode); + } + + public Collection> getLoadedClasses() { + return this.loadedClasses; + } + + public ClassCollector creatClassCallback(BiConsumer> creatClassCallback) { + this.creatClassCallback = creatClassCallback; + return this; + } + } + + public static class InnerLoader extends GroovyScriptClassLoader { + + private final GroovyScriptClassLoader delegate; + + public InnerLoader(GroovyScriptClassLoader delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override + public @Nullable URL loadResource(String name) throws MalformedURLException { + return delegate.loadResource(name); + } + + @Override + public void addClasspath(String path) { + delegate.addClasspath(path); + } + + @Override + public void clearCache() { + delegate.clearCache(); + } + + @Override + public URL findResource(String name) { + return delegate.findResource(name); + } + + @Override + public Enumeration findResources(String name) throws IOException { + return delegate.findResources(name); + } + + @Override + public Class[] getLoadedClasses() { + return delegate.getLoadedClasses(); + } + + @Override + public URL getResource(String name) { + return delegate.getResource(name); + } + + @Override + public InputStream getResourceAsStream(String name) { + return delegate.getResourceAsStream(name); + } + + @Override + public GroovyResourceLoader getResourceLoader() { + return delegate.getResourceLoader(); + } + + @Override + public URL[] getURLs() { + return delegate.getURLs(); + } + + @Override + public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException { + Class c = findLoadedClass(name); + if (c != null) return c; + return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript, resolve); + } + + @Override + public Class parseClass(GroovyCodeSource codeSource, boolean shouldCache) throws CompilationFailedException { + return delegate.parseClass(codeSource, shouldCache); + } + + @Override + public void setResourceLoader(GroovyResourceLoader resourceLoader) { + // no need to set a rl + // it's delegated anyway + } + + @Override + public void addURL(URL url) { + delegate.addURL(url); + } + + @Override + public Class defineClass(ClassNode classNode, String file, String newCodeBase) { + return delegate.defineClass(classNode, file, newCodeBase); + } + + @Override + public Class parseClass(File file) throws CompilationFailedException, IOException { + return delegate.parseClass(file); + } + + @Override + public Class parseClass(String text, String fileName) throws CompilationFailedException { + return delegate.parseClass(text, fileName); + } + + @Override + public Class parseClass(String text) throws CompilationFailedException { + return delegate.parseClass(text); + } + + @Override + public String generateScriptName() { + return delegate.generateScriptName(); + } + + @Override + public Class parseClass(Reader reader, String fileName) throws CompilationFailedException { + return delegate.parseClass(reader, fileName); + } + + @Override + public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException { + return delegate.parseClass(codeSource); + } + + @Override + public Class defineClass(String name, byte[] b) { + return delegate.defineClass(name, b); + } + + @Override + public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript) throws ClassNotFoundException, CompilationFailedException { + return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript); + } + + @Override + public void setShouldRecompile(Boolean mode) { + // it's delegated anyway + } + + @Override + public Boolean isShouldRecompile() { + return delegate.isShouldRecompile(); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return delegate.loadClass(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + return delegate.getResources(name); + } + + @Override + public void setDefaultAssertionStatus(boolean enabled) { + delegate.setDefaultAssertionStatus(enabled); + } + + @Override + public void setPackageAssertionStatus(String packageName, boolean enabled) { + delegate.setPackageAssertionStatus(packageName, enabled); + } + + @Override + public void setClassAssertionStatus(String className, boolean enabled) { + delegate.setClassAssertionStatus(className, enabled); + } + + @Override + public void clearAssertionStatus() { + delegate.clearAssertionStatus(); + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + delegate.close(); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index 811ba19e3..3545b98ab 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -3,71 +3,59 @@ import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.INamed; import com.cleanroommc.groovyscript.compat.mods.ModSupport; import com.cleanroommc.groovyscript.event.GroovyEventManager; import com.cleanroommc.groovyscript.event.GroovyReloadEvent; import com.cleanroommc.groovyscript.event.ScriptRunEvent; +import com.cleanroommc.groovyscript.helper.Alias; import com.cleanroommc.groovyscript.helper.GroovyHelper; -import com.cleanroommc.groovyscript.helper.JsonHelper; import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import groovy.lang.*; -import groovy.util.GroovyScriptEngine; +import groovy.lang.Binding; +import groovy.lang.Closure; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.Script; import groovy.util.ResourceException; import groovy.util.ScriptException; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.util.math.MathHelper; import net.minecraftforge.common.MinecraftForge; -import org.apache.commons.io.FileUtils; import org.apache.groovy.internal.util.UncheckedThrow; import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.runtime.InvokerInvocationException; -import org.codehaus.groovy.vmplugin.VMPlugin; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; -import java.net.URL; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -public class GroovyScriptSandbox extends GroovySandbox { - - /** - * Changing this number will force the cache to be deleted and every script has to be recompiled. - * Useful when changes to the compilation process were made. - */ - public static final int CACHE_VERSION = 3; - /** - * Setting this to false will cause compiled classes to never be cached. - * As a side effect some compilation behaviour might change. Can be useful for debugging. - */ - public static final boolean ENABLE_CACHE = true; - /** - * Setting this to true will cause the cache to be deleted before each script run. - * Useful for debugging. - */ - public static final boolean DELETE_CACHE_ON_RUN = Boolean.parseBoolean(System.getProperty("groovyscript.disable_cache")); - - private final File cacheRoot; - private final File scriptRoot; - private final Map, AtomicInteger> storedExceptions; - private final Map index = new Object2ObjectOpenHashMap<>(); +public class GroovyScriptSandbox { + private final CustomGroovyScriptEngine engine; + + private String currentScript; private LoadStage currentLoadStage; - @ApiStatus.Internal + private final ThreadLocal running = ThreadLocal.withInitial(() -> false); + private final Map bindings = new Object2ObjectOpenHashMap<>(); + private final ImportCustomizer importCustomizer = new ImportCustomizer(); + private final Map, AtomicInteger> storedExceptions = new Object2ObjectOpenHashMap<>(); + + private long compileTime; + private long runTime; + public GroovyScriptSandbox() { - super(SandboxData.getRootUrls()); - this.scriptRoot = SandboxData.getScriptFile(); - this.cacheRoot = SandboxData.getCachePath(); + CompilerConfiguration config = new CompilerConfiguration(); + initEngine(config); + this.engine = new CustomGroovyScriptEngine(SandboxData.getRootUrls(), SandboxData.getCachePath(), SandboxData.getScriptFile(), config); registerBinding("Mods", ModSupport.INSTANCE); registerBinding("Log", GroovyLog.get()); registerBinding("EventManager", GroovyEventManager.INSTANCE); @@ -104,88 +92,83 @@ public GroovyScriptSandbox() { "com.cleanroommc.groovyscript.event.EventBusType", "net.minecraftforge.fml.relauncher.Side", "net.minecraftforge.fml.relauncher.SideOnly"); - this.storedExceptions = new Object2ObjectOpenHashMap<>(); - readIndex(); } - private void readIndex() { - this.index.clear(); - JsonElement jsonElement = JsonHelper.loadJson(new File(this.cacheRoot, "_index.json")); - if (jsonElement == null || !jsonElement.isJsonObject()) return; - JsonObject json = jsonElement.getAsJsonObject(); - int cacheVersion = json.get("version").getAsInt(); - String java = json.has("java") ? json.get("java").getAsString() : ""; - if (cacheVersion != CACHE_VERSION || !java.equals(VMPlugin.getJavaVersion())) { - // cache version changed -> force delete cache - deleteScriptCache(); - return; - } - for (JsonElement element : json.getAsJsonArray("index")) { - if (element.isJsonObject()) { - CompiledScript cs = CompiledScript.fromJson(element.getAsJsonObject(), this.scriptRoot.getPath(), this.cacheRoot.getPath()); - if (cs != null) { - this.index.put(cs.path, cs); - } - } - } + protected Binding createBindings() { + Binding binding = new Binding(this.bindings); + postInitBindings(binding); + return binding; } - private void writeIndex() { - if (!ENABLE_CACHE) return; - JsonObject json = new JsonObject(); - json.addProperty("!DANGER!", "DO NOT EDIT THIS FILE!!!"); - json.addProperty("version", CACHE_VERSION); - json.addProperty("java", VMPlugin.getJavaVersion()); - JsonArray index = new JsonArray(); - json.add("index", index); - for (Map.Entry entry : this.index.entrySet()) { - index.add(entry.getValue().toJson()); + public void registerBinding(String name, Object obj) { + Objects.requireNonNull(name); + Objects.requireNonNull(obj); + for (String alias : Alias.generateOf(name)) { + bindings.put(alias, obj); } - JsonHelper.saveJson(new File(this.cacheRoot, "_index.json"), json); } - public void checkSyntax() { - GroovyScriptEngine engine = createScriptEngine(); - Binding binding = createBindings(); - Set executedClasses = new ObjectOpenHashSet<>(); - - for (LoadStage loadStage : LoadStage.getLoadStages()) { - GroovyLog.get().info("Checking syntax in loader '{}'", this.currentLoadStage); - this.currentLoadStage = loadStage; - load(engine, binding, executedClasses, false); + public void registerBinding(INamed named) { + Objects.requireNonNull(named); + for (String alias : named.getAliases()) { + bindings.put(alias, named); } } public void run(LoadStage currentLoadStage) { this.currentLoadStage = Objects.requireNonNull(currentLoadStage); try { - super.load(); + load(); } catch (IOException | ScriptException | ResourceException e) { GroovyLog.get().exception("An exception occurred while trying to run groovy code! This is might be a internal groovy issue.", e); } catch (Throwable t) { GroovyLog.get().exception(t); } finally { + GroovyLog.get().infoMC("Groovy scripts took {}ms to compile and {}ms to run in {}.", this.compileTime, this.runTime, currentLoadStage.getName()); this.currentLoadStage = null; if (currentLoadStage == LoadStage.POST_INIT) { - writeIndex(); + engine.writeIndex(); } } } - @Override protected void runScript(Script script) { - GroovyLog.get().info(" - running {}", script.getClass().getName()); - super.runScript(script); + GroovyLog.get().info(" - running script {}", script.getClass().getName()); + setCurrentScript(script.getClass().getName()); + try { + script.run(); + } finally { + setCurrentScript(null); + } } - @ApiStatus.Internal - @Override - public void load() throws Exception { - throw new UnsupportedOperationException("Use run(Loader loader) instead!"); + protected void runClass(Class script) { + GroovyLog.get().info(" - loading class {}", script.getName()); + setCurrentScript(script.getName()); + try { + // $getLookup is present on all groovy created classes + // call it cause the class to be initialised + Method m = script.getMethod("$getLookup"); + m.invoke(null); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + GroovyLog.get().errorMC("Error initialising class '{}'", script); + } finally { + setCurrentScript(null); + } + } + + public void checkSyntax() { + Binding binding = createBindings(); + Set executedClasses = new ObjectOpenHashSet<>(); + + for (LoadStage loadStage : LoadStage.getLoadStages()) { + GroovyLog.get().info("Checking syntax in loader '{}'", this.currentLoadStage); + this.currentLoadStage = loadStage; + load(binding, executedClasses, false); + } } @ApiStatus.Internal - @Override public T runClosure(Closure closure, Object... args) { boolean wasRunning = isRunning(); if (!wasRunning) startRunning(); @@ -226,113 +209,84 @@ private static T runClosureInternal(Closure closure, Object[] args) throw } } - private static String mainClassName(String name) { - return name.contains("$") ? name.split("\\$", 2)[0] : name; - } + private void load() throws Exception { + preRun(); - /** - * Called via mixin when groovy compiled a class from scripts. - */ - @ApiStatus.Internal - public void onCompileClass(SourceUnit su, String path, Class clazz, byte[] code, boolean inner) { - String shortPath = FileUtil.relativize(this.scriptRoot.getPath(), path); - // if the script was compiled because another script depends on it, the source unit is wrong - // we need to find the source unit of the compiled class - SourceUnit trueSource = su.getAST().getUnit().getScriptSourceLocation(mainClassName(clazz.getName())); - String truePath = trueSource == null ? shortPath : FileUtil.relativize(this.scriptRoot.getPath(), trueSource.getName()); - if (shortPath.equals(truePath) && su.getAST().getMainClassName() != null && !su.getAST().getMainClassName().equals(clazz.getName())) { - inner = true; + Binding binding = createBindings(); + Set executedClasses = new ObjectOpenHashSet<>(); + + this.running.set(true); + try { + load(binding, executedClasses, true); + } finally { + this.running.set(false); + postRun(); + setCurrentScript(null); } + } - boolean finalInner = inner; - CompiledScript comp = this.index.computeIfAbsent(truePath, k -> new CompiledScript(k, finalInner ? -1 : 0)); - CompiledClass innerClass = comp; - if (inner) innerClass = comp.findInnerClass(clazz.getName()); - innerClass.onCompile(code, clazz, this.cacheRoot.getPath()); + protected void load(Binding binding, Set executedClasses, boolean run) { + this.compileTime = 0L; + this.runTime = 0L; + // now run all script files + loadScripts(binding, executedClasses, run); } - /** - * Called via mixin when a script class needs to be recompiled. This happens when a script was loaded because another script depends on - * it. Groovy will then try to compile the script again. If we already compiled the class we just stop the compilation process. - */ - @ApiStatus.Internal - public Class onRecompileClass(URL source, String className) { - String path = source.toExternalForm(); - String rel = FileUtil.relativize(this.scriptRoot.getPath(), path); - CompiledScript cs = this.index.get(rel); - Class c = null; - if (cs != null) { - if (cs.clazz == null && cs.readData(this.cacheRoot.getPath())) { - cs.ensureLoaded(getClassLoader(), this.cacheRoot.getPath()); + protected void loadScripts(Binding binding, Set executedClasses, boolean run) { + for (CompiledScript compiledScript : this.engine.findScripts(getScriptFiles())) { + if (!executedClasses.contains(compiledScript.path)) { + long t = System.currentTimeMillis(); + this.engine.loadScript(compiledScript); + this.compileTime += System.currentTimeMillis() - t; + if (compiledScript.preprocessorCheckFailed()) continue; + if (compiledScript.clazz == null) { + GroovyLog.get().errorMC("Error loading script {}", compiledScript.path); + continue; + } + if (compiledScript.clazz.getSuperclass() != Script.class) { + // script is a class + if (run && shouldRunFile(compiledScript.path)) { + t = System.currentTimeMillis(); + runClass(compiledScript.clazz); + this.runTime += System.currentTimeMillis() - t; + } + executedClasses.add(compiledScript.path); + continue; + } + if (run && shouldRunFile(compiledScript.path)) { + Script script = InvokerHelper.createScript(compiledScript.clazz, binding); + t = System.currentTimeMillis(); + runScript(script); + this.runTime += System.currentTimeMillis() - t; + } } - c = cs.clazz; } - return c; } - @Override - protected Class loadScriptClass(GroovyScriptEngine engine, File file) { - String relativeFileName = FileUtil.relativize(this.scriptRoot.getPath(), file.getPath()); - File relativeFile = new File(relativeFileName); - long lastModified = file.lastModified(); - CompiledScript comp = this.index.get(relativeFileName); - - if (ENABLE_CACHE && comp != null && lastModified <= comp.lastEdited && comp.clazz == null && comp.readData(this.cacheRoot.getPath())) { - // class is not loaded, but the cached class bytes are still valid - if (!comp.checkPreprocessors(this.scriptRoot)) { - return GroovyLog.class; // failed preprocessor check - } - comp.ensureLoaded(getClassLoader(), this.cacheRoot.getPath()); + protected void startRunning() { + this.running.set(true); + } - } else if (!ENABLE_CACHE || (comp == null || comp.clazz == null || lastModified > comp.lastEdited)) { - // class is not loaded and class bytes don't exist yet or script has been edited - if (comp == null) { - comp = new CompiledScript(relativeFileName, 0); - this.index.put(relativeFileName, comp); - } - if (lastModified > comp.lastEdited || comp.preprocessors == null) { - // recompile preprocessors if there is no data or script was edited - comp.preprocessors = Preprocessor.parsePreprocessors(file); - } - comp.lastEdited = lastModified; - if (!comp.checkPreprocessors(this.scriptRoot)) { - // delete class bytes to make sure it's recompiled once the preprocessors returns true - comp.deleteCache(this.cacheRoot.getPath()); - comp.clazz = null; - comp.data = null; - return GroovyLog.class; // failed preprocessor check - } - Class clazz = super.loadScriptClass(engine, relativeFile); - if (comp.clazz == null) { - // should not happen - GroovyLog.get().errorMC("Class for {} was loaded, but didn't receive class created callback!", relativeFileName); - if (ENABLE_CACHE) comp.clazz = clazz; - } - } else { - // class is loaded and script wasn't edited - if (!comp.checkPreprocessors(this.scriptRoot)) { - return GroovyLog.class; // failed preprocessor check - } - comp.ensureLoaded(getClassLoader(), this.cacheRoot.getPath()); - } - return comp.clazz; + protected void stopRunning() { + this.running.set(false); } - @Override + @ApiStatus.OverrideOnly protected void postInitBindings(Binding binding) { binding.setProperty("out", GroovyLog.get().getWriter()); binding.setVariable("globals", getBindings()); } - @Override - protected void initEngine(GroovyScriptEngine engine, CompilerConfiguration config) { + @ApiStatus.OverrideOnly + protected void initEngine(CompilerConfiguration config) { + config.addCompilationCustomizers(this.importCustomizer); config.addCompilationCustomizers(new GroovyScriptCompiler()); config.addCompilationCustomizers(new GroovyScriptEarlyCompiler()); } - @Override + @ApiStatus.OverrideOnly protected void preRun() { - if (DELETE_CACHE_ON_RUN) deleteScriptCache(); + if (CustomGroovyScriptEngine.DELETE_CACHE_ON_RUN) this.engine.deleteScriptCache(); // first clear all added events GroovyEventManager.INSTANCE.reset(); if (this.currentLoadStage.isReloadable() && !ReloadableRegistryManager.isFirstLoad()) { @@ -342,17 +296,17 @@ protected void preRun() { MinecraftForge.EVENT_BUS.post(new GroovyReloadEvent()); } GroovyLog.get().infoMC("Running scripts in loader '{}'", this.currentLoadStage); + //this.engine.prepareEngine(this.currentLoadStage); // and finally invoke pre script run event MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(this.currentLoadStage)); } - @Override - protected boolean shouldRunFile(File file) { - //GroovyLog.get().info(" - executing {}", file.toString()); + @ApiStatus.OverrideOnly + protected boolean shouldRunFile(String file) { return true; } - @Override + @ApiStatus.OverrideOnly protected void postRun() { if (this.currentLoadStage == LoadStage.POST_INIT) { ReloadableRegistryManager.afterScriptRun(); @@ -363,34 +317,47 @@ protected void postRun() { } } - @Override - public Collection getClassFiles() { - return GroovyScript.getRunConfig().getClassFiles(this.scriptRoot, this.currentLoadStage.getName()); + public File getScriptRoot() { + return SandboxData.getScriptFile(); } - @Override public Collection getScriptFiles() { - return GroovyScript.getRunConfig().getSortedFiles(this.scriptRoot, this.currentLoadStage.getName()); + return GroovyScript.getRunConfig().getSortedFiles(getScriptRoot(), this.currentLoadStage.getName()); } - public @Nullable LoadStage getCurrentLoader() { + public boolean isRunning() { + return this.running.get(); + } + + public Map getBindings() { + return bindings; + } + + public ImportCustomizer getImportCustomizer() { + return importCustomizer; + } + + public CustomGroovyScriptEngine getEngine() { + return engine; + } + + public String getCurrentScript() { + return currentScript; + } + + protected void setCurrentScript(String currentScript) { + this.currentScript = currentScript; + } + + public LoadStage getCurrentLoader() { return currentLoadStage; } - public File getScriptRoot() { - return scriptRoot; + public long getLastCompileTime() { + return compileTime; } - @ApiStatus.Internal - public boolean deleteScriptCache() { - this.index.clear(); - getClassLoader().clearCache(); - try { - FileUtils.cleanDirectory(this.cacheRoot); - return true; - } catch (IOException e) { - GroovyScript.LOGGER.throwing(e); - return false; - } + public long getLastRunTime() { + return runTime; } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java index bc3d5173b..4b215d81e 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java @@ -31,14 +31,11 @@ public static JsonObject createDefaultJson() { json.addProperty("packId", "placeholdername"); json.addProperty("version", "1.0.0"); json.addProperty("debug", false); - JsonObject classes = new JsonObject(); - JsonArray preInit = new JsonArray(); - classes.add("preInit", preInit); - json.add("classes", classes); JsonObject loaders = new JsonObject(); json.add("loaders", loaders); - preInit = new JsonArray(); + JsonArray preInit = new JsonArray(); loaders.add("preInit", preInit); + preInit.add("classes/"); preInit.add("preInit/"); JsonArray postInit = new JsonArray(); loaders.add("postInit", postInit); @@ -65,7 +62,6 @@ public static JsonObject createDefaultJson() { private final String packName; private final String packId; private final String version; - private final Map> classes = new Object2ObjectOpenHashMap<>(); private final Map> loaderPaths = new Object2ObjectOpenHashMap<>(); private final List packmodeList = new ArrayList<>(); private final Set packmodeSet = new ObjectOpenHashSet<>(); @@ -109,7 +105,6 @@ public void reload(JsonObject json, boolean init) { throw new RuntimeException(); } this.debug = JsonHelper.getBoolean(json, false, "debug"); - this.classes.clear(); this.loaderPaths.clear(); this.packmodeList.clear(); this.packmodeSet.clear(); @@ -119,31 +114,9 @@ public void reload(JsonObject json, boolean init) { String regex = File.separatorChar == '\\' ? "/" : "\\\\"; String replacement = getSeparator(); if (json.has("classes")) { - JsonElement jsonClasses = json.get("classes"); - - if (jsonClasses.isJsonArray()) { - List classes = this.classes.computeIfAbsent("all", key -> new ArrayList<>()); - for (JsonElement element : jsonClasses.getAsJsonArray()) { - classes.add(sanitizePath(element.getAsString().replaceAll(regex, replacement))); - } - } else if (jsonClasses.isJsonObject()) { - for (Map.Entry entry : jsonClasses.getAsJsonObject().entrySet()) { - List classes = this.classes.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()); - if (entry.getValue().isJsonPrimitive()) { - classes.add(sanitizePath(entry.getValue().getAsString().replaceAll(regex, replacement))); - } else if (entry.getValue().isJsonArray()) { - for (JsonElement element : entry.getValue().getAsJsonArray()) { - classes.add(sanitizePath(element.getAsString().replaceAll(regex, replacement))); - } - } - if (classes.isEmpty()) { - this.classes.remove(entry.getKey()); - } - } - } + throw new IllegalStateException("GroovyScript classes definition in runConfig is deprecated! Classes are now treated as normal scripts."); } - JsonObject jsonLoaders = JsonHelper.getJsonObject(json, "loaders"); List> pathsList = new ArrayList<>(); @@ -279,21 +252,10 @@ public int getPackmodeConfigState() { } public boolean isLoaderConfigured(String loader) { - List path = this.classes.get(loader); - if (path != null && !path.isEmpty()) return true; - path = this.loaderPaths.get(loader); + List path = this.loaderPaths.get(loader); return path != null && !path.isEmpty(); } - public Collection getClassFiles(File root, String loader) { - List paths = this.classes.get("all"); - paths = paths == null ? new ArrayList<>() : new ArrayList<>(paths); - if (this.classes.containsKey(loader)) { - paths.addAll(this.classes.get(loader)); - } - return SandboxData.getSortedFilesOf(root, paths); - } - public Collection getSortedFiles(File root, String loader) { List paths = loaderPaths.get(loader); if (paths == null || paths.isEmpty()) return Collections.emptyList(); diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java index 995dff1d5..d6c07dbe5 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java @@ -7,6 +7,7 @@ import com.cleanroommc.groovyscript.sandbox.security.GroovySecurityManager; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; +import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.reflection.*; @@ -88,17 +89,34 @@ private static CachedMethod makeMethod(CachedClass cachedClass, Method method, b return new CachedMethod(cachedClass, method); } + public static boolean inheritsMCClas(ClassNode classNode) { + do { + if (classNode.getName().startsWith(MC_CLASS)) { + return true; + } + ClassNode[] interfaces = classNode.getInterfaces(); + if (interfaces != null) { + for (ClassNode iface : interfaces) { + if (inheritsMCClas(iface)) { + return true; + } + } + } + classNode = classNode.getSuperClass(); + } while (classNode != null && classNode != ClassHelper.OBJECT_TYPE); + return false; + } + /** * This bad boy is responsible for remapping overriden methods. Called via Mixin */ public static void remapOverrides(ClassNode classNode) { if (FMLLaunchHandler.isDeobfuscatedEnvironment()) return; - ClassNode superClass = classNode.getSuperClass(); - if (superClass == null || !superClass.getName().startsWith(MC_CLASS)) return; + if (!inheritsMCClas(classNode)) return; List methodNodes = classNode.getMethods(); for (int i = 0, methodNodesSize = methodNodes.size(); i < methodNodesSize; i++) { MethodNode methodNode = methodNodes.get(i); - String obf = GroovyDeobfMapper.getObfuscatedMethodName(superClass, methodNode.getName(), methodNode.getParameters()); + String obf = GroovyDeobfMapper.getObfuscatedMethodName(classNode, methodNode.getName(), methodNode.getParameters()); if (obf != null) { classNode.addMethod(copyRemappedMethodNode(obf, methodNode)); } diff --git a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCompilationUnitFactory.java b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCompilationUnitFactory.java index c21fd7232..00a87e1f4 100644 --- a/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCompilationUnitFactory.java +++ b/src/main/java/com/cleanroommc/groovyscript/server/GroovyScriptCompilationUnitFactory.java @@ -1,7 +1,6 @@ package com.cleanroommc.groovyscript.server; import com.cleanroommc.groovyscript.GroovyScript; -import com.cleanroommc.groovyscript.sandbox.LoadStage; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler; import groovy.lang.GroovyClassLoader; @@ -17,7 +16,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; -import java.util.stream.Stream; public class GroovyScriptCompilationUnitFactory extends CompilationUnitFactoryBase { @@ -54,7 +52,7 @@ public GroovyLSCompilationUnit create(Path workspaceRoot, @Nullable URI context) context = null; // actions on classes are going into classes only unit } - var unit = compilationUnitsByScript.computeIfAbsent(context, uri -> new GroovyLSCompilationUnit(getConfiguration(), null, getClassLoader(), languageServerContext)); + var unit = compilationUnitsByScript.computeIfAbsent(context, uri -> new GroovyLSCompilationUnit(getConfiguration(), null, GroovyScript.getSandbox().getEngine().getClassLoader(), languageServerContext)); var changedUris = languageServerContext.getFileContentsTracker().getChangedURIs(); @@ -74,11 +72,11 @@ public GroovyLSCompilationUnit create(Path workspaceRoot, @Nullable URI context) }); // add all other classes too - getAllClasses() + /*getAllClasses() .filter(path -> !languageServerContext.getFileContentsTracker().isOpen(path.toUri())) .forEach(path -> { addOpenFileToCompilationUnit(path.toUri(), languageServerContext.getFileContentsTracker().getContents(path.toUri()), unit); - }); + });*/ if (context != null) { var contents = languageServerContext.getFileContentsTracker().getContents(context); @@ -91,19 +89,20 @@ public GroovyLSCompilationUnit create(Path workspaceRoot, @Nullable URI context) } protected boolean isInClassesContext(URI uri) { - var file = Paths.get(uri).getParent(); + return false; + //var file = Paths.get(uri).getParent(); - return getAllClasses().anyMatch(file::startsWith); + //return getAllClasses().anyMatch(file::startsWith); } - protected Stream getAllClasses() { + /*protected Stream getAllClasses() { return LoadStage.getLoadStages() .stream() .map(LoadStage::getName) .flatMap(loader -> GroovyScript.getRunConfig().getClassFiles(this.root, loader).stream()) .map(File::toPath) .map(path -> GroovyScript.getScriptFile().toPath().resolve(path)); - } + }*/ protected void removeSources(GroovyLSCompilationUnit unit, Set urisToRemove) { List sourcesToRemove = new ArrayList<>(); @@ -114,7 +113,7 @@ protected void removeSources(GroovyLSCompilationUnit unit, Set urisToRemove } }); - // if an URI has changed, we remove it from the compilation unit so + // if a URI has changed, we remove it from the compilation unit so // that a new version can be built from the updated source file unit.removeSources(sourcesToRemove); } diff --git a/src/main/java/net/prominic/groovyls/compiler/ILanguageServerContext.java b/src/main/java/net/prominic/groovyls/compiler/ILanguageServerContext.java index 60470c34d..328d45a3f 100644 --- a/src/main/java/net/prominic/groovyls/compiler/ILanguageServerContext.java +++ b/src/main/java/net/prominic/groovyls/compiler/ILanguageServerContext.java @@ -1,13 +1,13 @@ package net.prominic.groovyls.compiler; -import com.cleanroommc.groovyscript.sandbox.GroovySandbox; +import com.cleanroommc.groovyscript.sandbox.GroovyScriptSandbox; import io.github.classgraph.ScanResult; import net.prominic.groovyls.compiler.documentation.DocumentationFactory; import net.prominic.groovyls.util.FileContentsTracker; public interface ILanguageServerContext { - GroovySandbox getSandbox(); + GroovyScriptSandbox getSandbox(); ScanResult getScanResult(); diff --git a/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java b/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java index c55352ad0..86e3af874 100644 --- a/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/CompletionProvider.java @@ -19,11 +19,13 @@ //////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.providers; +import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.mapper.AbstractObjectMapper; import com.cleanroommc.groovyscript.server.CompletionParams; import com.cleanroommc.groovyscript.server.Completions; import groovy.lang.Closure; import groovy.lang.DelegatesTo; +import groovy.lang.Script; import io.github.classgraph.ClassInfo; import io.github.classgraph.FieldInfo; import io.github.classgraph.MethodInfo; @@ -289,9 +291,9 @@ private void populateItemsFromClassNode(ClassNode classNode, Position position, if (classRange == null) return; String className = getMemberName(classNode.getUnresolvedName(), classRange, position); if (classNode.equals(parentClassNode.getUnresolvedSuperClass())) { - populateTypes(classNode, className, new HashSet<>(), true, false, false, items); + populateTypes(classNode, className, new ObjectOpenHashSet<>(), true, false, false, items); } else if (Arrays.asList(parentClassNode.getUnresolvedInterfaces()).contains(classNode)) { - populateTypes(classNode, className, new HashSet<>(), false, true, false, items); + populateTypes(classNode, className, new ObjectOpenHashSet<>(), false, true, false, items); } } @@ -301,7 +303,7 @@ private void populateItemsFromConstructorCallExpression(ConstructorCallExpressio Range typeRange = GroovyLSUtils.astNodeToRange(constructorCallExpr.getType()); if (typeRange == null) return; String typeName = getMemberName(constructorCallExpr.getType().getNameWithoutPackage(), typeRange, position); - populateTypes(constructorCallExpr, typeName, new HashSet<>(), true, false, false, items); + populateTypes(constructorCallExpr, typeName, new ObjectOpenHashSet<>(), true, false, false, items); } private void populateItemsFromVariableExpression(VariableExpression varExpr, Position position, Completions items) { @@ -319,6 +321,7 @@ private void populateItemsFromPropertiesAndFields(List properties, String name = p.getName(); if (!p.isPublic() || existingNames.contains(name)) return null; existingNames.add(name); + if (p.getDeclaringClass().isDerivedFrom(ClassHelper.makeCached(Script.class)) && p.getName().equals("__$stMC")) return null; CompletionItem item = CompletionItemFactory.createCompletion(p, p.getName(), astContext); if (!p.isDynamicTyped()) { var details = new CompletionItemLabelDetails(); @@ -331,6 +334,7 @@ private void populateItemsFromPropertiesAndFields(List properties, String name = f.getName(); if (!f.isPublic() || existingNames.contains(name)) return null; existingNames.add(name); + if (f.getDeclaringClass().isDerivedFrom(ClassHelper.makeCached(Script.class)) && f.getName().equals("__$stMC")) return null; CompletionItem item = CompletionItemFactory.createCompletion(f, f.getName(), astContext); if (!f.isDynamicTyped()) { var details = new CompletionItemLabelDetails(); @@ -346,6 +350,9 @@ private void populateItemsFromMethods(List methods, Set exis String name = getDescriptor(method, true, false, false); if (!method.isPublic() || existingNames.contains(name)) return null; existingNames.add(name); + if (method.getDeclaringClass().isDerivedFrom(ClassHelper.makeCached(Script.class))) { + if (method.getName().equals("$getLookup") || method.getName().equals("main")) return null; + } if (method.getDeclaringClass().isResolved() && (method.getModifiers() & GroovyASTUtils.EXPANSION_MARKER) == 0 && GroovyReflectionUtils.resolveMethodFromMethodNode(method, astContext) == null) { return null; } @@ -440,7 +447,9 @@ public static String getDescriptor(MethodInfo node, boolean includeName, boolean } builder.append(")"); if (!includeReturn) return builder.toString(); - var ret = display ? node.getTypeSignatureOrTypeDescriptor().getResultType().toStringWithSimpleNames() : node.getTypeDescriptor().getResultType().toString(); + var ret = display + ? node.getTypeSignatureOrTypeDescriptor().getResultType().toStringWithSimpleNames() + : node.getTypeDescriptor().getResultType().toString(); if (!ret.equals("void")) { if (display) builder.append(" -> "); builder.append(ret); @@ -449,7 +458,8 @@ public static String getDescriptor(MethodInfo node, boolean includeName, boolean } public static StringBuilder appendParameter(MethodParameterInfo param, StringBuilder builder, boolean display, boolean maybeVarargs) { - builder.append(display ? param.getTypeSignatureOrTypeDescriptor().toStringWithSimpleNames() : param.getTypeDescriptor().toString()); // don't use generic types + builder.append( + display ? param.getTypeSignatureOrTypeDescriptor().toStringWithSimpleNames() : param.getTypeDescriptor().toString()); // don't use generic types if (maybeVarargs && builder.charAt(builder.length() - 1) == ']' && builder.charAt(builder.length() - 2) == '[') { builder.delete(builder.length() - 2, builder.length()).append("..."); } @@ -627,7 +637,14 @@ private void populateTypes(ASTNode offsetNode, ModuleNode enclosingModule = getModule(); String enclosingPackageName = enclosingModule.getPackageName(); - items.addAll(astContext.getVisitor().getClassNodes(), classNode -> { + List classNodes = astContext.getVisitor().getClassNodes(); + Set all = new ObjectOpenHashSet<>(); + all.addAll(classNodes); + for (Class clz : GroovyScript.getSandbox().getEngine().getAllLoadedScriptClasses()) { + //if (Script.class.isAssignableFrom(clz)) continue; + all.add(ClassHelper.makeCached(clz)); + } + items.addAll(all, classNode -> { if (!includeEnums && classNode.isEnum()) return null; if (!includeInterfaces && classNode.isInterface()) return null; if (!includeClasses && (!classNode.isInterface() && !classNode.isEnum())) return null; diff --git a/src/main/resources/mixin.groovyscript.json b/src/main/resources/mixin.groovyscript.json index 6b3d2a423..5ca5064f1 100644 --- a/src/main/resources/mixin.groovyscript.json +++ b/src/main/resources/mixin.groovyscript.json @@ -23,10 +23,8 @@ "TileEntityPistonMixin", "VillagerProfessionAccessor", "groovy.AsmDecompilerMixin", - "groovy.ClassCollectorMixin", "groovy.ClosureMixin", "groovy.CompUnitClassGenMixin", - "groovy.GroovyClassLoaderMixin", "groovy.Java8Mixin", "groovy.MetaClassImplMixin", "groovy.ModuleNodeAccessor",