From 0078a8cb8e76e62ed0fdf9beeca762b25cedf92c Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 9 Dec 2019 12:48:15 -0500 Subject: [PATCH] Fixed scripts not working on older Android phones --- android/build.gradle | 2 +- .../io/anuke/mindustry/AndroidLauncher.java | 4 +- .../rhino/AndroidContextFactory.java | 44 +++++++++ .../rhino/BaseAndroidClassLoader.java | 99 +++++++++++++++++++ .../rhino/FileAndroidClassLoader.java | 59 +++++++++++ .../rhino/InMemoryAndroidClassLoader.java | 42 ++++++++ .../anuke/mindustry/rhino/RhinoBuilder.java | 83 ++++++++++++++++ core/assets/bundles/bundle.properties | 1 + core/src/io/anuke/mindustry/mod/Mods.java | 4 + core/src/io/anuke/mindustry/mod/Scripts.java | 17 +++- 10 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 android/src/io/anuke/mindustry/rhino/AndroidContextFactory.java create mode 100644 android/src/io/anuke/mindustry/rhino/BaseAndroidClassLoader.java create mode 100644 android/src/io/anuke/mindustry/rhino/FileAndroidClassLoader.java create mode 100644 android/src/io/anuke/mindustry/rhino/InMemoryAndroidClassLoader.java create mode 100644 android/src/io/anuke/mindustry/rhino/RhinoBuilder.java diff --git a/android/build.gradle b/android/build.gradle index 5d438dc30c..5795f34340 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -28,7 +28,7 @@ dependencies{ implementation project(":core") implementation arcModule("backends:backend-android") - implementation 'com.faendir.rhino:rhino-android:1.5.2' + implementation 'com.jakewharton.android.repackaged:dalvik-dx:9.0.0_r3' natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a" diff --git a/android/src/io/anuke/mindustry/AndroidLauncher.java b/android/src/io/anuke/mindustry/AndroidLauncher.java index e8e3223a19..0faeba5a05 100644 --- a/android/src/io/anuke/mindustry/AndroidLauncher.java +++ b/android/src/io/anuke/mindustry/AndroidLauncher.java @@ -9,7 +9,6 @@ import android.os.Build.*; import android.os.*; import android.provider.Settings.*; import android.telephony.*; -import com.faendir.rhino_android.*; import io.anuke.arc.*; import io.anuke.arc.backends.android.surfaceview.*; import io.anuke.arc.files.*; @@ -20,6 +19,7 @@ import io.anuke.arc.util.serialization.*; import io.anuke.mindustry.game.Saves.*; import io.anuke.mindustry.io.*; import io.anuke.mindustry.mod.*; +import io.anuke.mindustry.rhino.*; import io.anuke.mindustry.ui.dialogs.*; import java.io.*; @@ -68,7 +68,7 @@ public class AndroidLauncher extends AndroidApplication{ @Override public org.mozilla.javascript.Context getScriptContext(){ - return new RhinoAndroidHelper(Core.files.local("script-output").file()).enterContext(); + return new RhinoBuilder(getContext()).enterContext(); } @Override diff --git a/android/src/io/anuke/mindustry/rhino/AndroidContextFactory.java b/android/src/io/anuke/mindustry/rhino/AndroidContextFactory.java new file mode 100644 index 0000000000..318af0c2d4 --- /dev/null +++ b/android/src/io/anuke/mindustry/rhino/AndroidContextFactory.java @@ -0,0 +1,44 @@ +package io.anuke.mindustry.rhino; + +import android.os.*; +import org.mozilla.javascript.*; + +import java.io.*; + +/** + * Ensures that the classLoader used is correct + * @author F43nd1r + * @since 11.01.2016 + */ +public class AndroidContextFactory extends ContextFactory{ + + private final File cacheDirectory; + + /** + * Create a new factory. It will cache generated code in the given directory + * @param cacheDirectory the cache directory + */ + public AndroidContextFactory(File cacheDirectory){ + this.cacheDirectory = cacheDirectory; + initApplicationClassLoader(createClassLoader(AndroidContextFactory.class.getClassLoader())); + } + + /** + * Create a ClassLoader which is able to deal with bytecode + * @param parent the parent of the create classloader + * @return a new ClassLoader + */ + @Override + public BaseAndroidClassLoader createClassLoader(ClassLoader parent){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ + return new InMemoryAndroidClassLoader(parent); + } + return new FileAndroidClassLoader(parent, cacheDirectory); + } + + @Override + protected void onContextReleased(final Context cx){ + super.onContextReleased(cx); + ((BaseAndroidClassLoader)cx.getApplicationClassLoader()).reset(); + } +} diff --git a/android/src/io/anuke/mindustry/rhino/BaseAndroidClassLoader.java b/android/src/io/anuke/mindustry/rhino/BaseAndroidClassLoader.java new file mode 100644 index 0000000000..83491956e8 --- /dev/null +++ b/android/src/io/anuke/mindustry/rhino/BaseAndroidClassLoader.java @@ -0,0 +1,99 @@ +package io.anuke.mindustry.rhino; + +import com.android.dex.*; +import com.android.dx.cf.direct.*; +import com.android.dx.command.dexer.*; +import com.android.dx.dex.*; +import com.android.dx.dex.cf.*; +import com.android.dx.dex.file.*; +import com.android.dx.merge.*; +import org.mozilla.javascript.*; + +import java.io.*; + +/** + * Compiles java bytecode to dex bytecode and loads it + * @author F43nd1r + * @since 11.01.2016 + */ +abstract class BaseAndroidClassLoader extends ClassLoader implements GeneratedClassLoader{ + + /** + * Create a new instance with the given parent classloader + * @param parent the parent + */ + public BaseAndroidClassLoader(ClassLoader parent){ + super(parent); + } + + /** + * {@inheritDoc} + */ + @Override + public Class defineClass(String name, byte[] data){ + try{ + DexOptions dexOptions = new DexOptions(); + DexFile dexFile = new DexFile(dexOptions); + DirectClassFile classFile = new DirectClassFile(data, name.replace('.', '/') + ".class", true); + classFile.setAttributeFactory(StdAttributeFactory.THE_ONE); + classFile.getMagic(); + DxContext context = new DxContext(); + dexFile.add(CfTranslator.translate(context, classFile, null, new CfOptions(), dexOptions, dexFile)); + Dex dex = new Dex(dexFile.toDex(null, false)); + Dex oldDex = getLastDex(); + if(oldDex != null){ + dex = new DexMerger(new Dex[]{dex, oldDex}, CollisionPolicy.KEEP_FIRST, context).merge(); + } + return loadClass(dex, name); + }catch(IOException | ClassNotFoundException e){ + throw new FatalLoadingException(e); + } + } + + protected abstract Class loadClass(Dex dex, String name) throws ClassNotFoundException; + + protected abstract Dex getLastDex(); + + protected abstract void reset(); + + /** + * Does nothing + * @param aClass ignored + */ + @Override + public void linkClass(Class aClass){ + //doesn't make sense on android + } + + /** + * Try to load a class. This will search all defined classes, all loaded jars and the parent class loader. + * @param name the name of the class to load + * @param resolve ignored + * @return the class + * @throws ClassNotFoundException if the class could not be found in any of the locations + */ + @Override + public Class loadClass(String name, boolean resolve) + throws ClassNotFoundException{ + Class loadedClass = findLoadedClass(name); + if(loadedClass == null){ + Dex dex = getLastDex(); + if(dex != null){ + loadedClass = loadClass(dex, name); + } + if(loadedClass == null){ + loadedClass = getParent().loadClass(name); + } + } + return loadedClass; + } + + /** + * Might be thrown in any Rhino method that loads bytecode if the loading failed + */ + public static class FatalLoadingException extends RuntimeException{ + FatalLoadingException(Throwable t){ + super("Failed to define class", t); + } + } +} diff --git a/android/src/io/anuke/mindustry/rhino/FileAndroidClassLoader.java b/android/src/io/anuke/mindustry/rhino/FileAndroidClassLoader.java new file mode 100644 index 0000000000..4f16618e84 --- /dev/null +++ b/android/src/io/anuke/mindustry/rhino/FileAndroidClassLoader.java @@ -0,0 +1,59 @@ +package io.anuke.mindustry.rhino; + +import com.android.dex.*; +import dalvik.system.*; +import io.anuke.arc.*; +import io.anuke.arc.backends.android.surfaceview.*; +import io.anuke.arc.util.ArcAnnotate.*; + +import java.io.*; + +/** + * @author F43nd1r + * @since 24.10.2017 + */ +@SuppressWarnings("ResultOfMethodCallIgnored") +class FileAndroidClassLoader extends BaseAndroidClassLoader{ + private static int instanceCounter = 0; + private final File dexFile; + + /** + * Create a new instance with the given parent classloader + * @param parent the parent + */ + public FileAndroidClassLoader(ClassLoader parent, File cacheDir){ + super(parent); + int id = instanceCounter++; + dexFile = new File(cacheDir, id + ".dex"); + cacheDir.mkdirs(); + reset(); + } + + @Override + protected Class loadClass(@NonNull Dex dex, @NonNull String name) throws ClassNotFoundException{ + try{ + dex.writeTo(dexFile); + }catch(IOException e){ + e.printStackTrace(); + } + return new DexClassLoader(dexFile.getPath(), ((AndroidApplication)Core.app).getContext().getCacheDir().getAbsolutePath(), null, getParent()).loadClass(name); + } + + @Nullable + @Override + protected Dex getLastDex(){ + if(dexFile.exists()){ + try{ + return new Dex(dexFile); + }catch(IOException e){ + e.printStackTrace(); + } + } + return null; + } + + @Override + protected void reset(){ + dexFile.delete(); + } +} diff --git a/android/src/io/anuke/mindustry/rhino/InMemoryAndroidClassLoader.java b/android/src/io/anuke/mindustry/rhino/InMemoryAndroidClassLoader.java new file mode 100644 index 0000000000..dd358d247f --- /dev/null +++ b/android/src/io/anuke/mindustry/rhino/InMemoryAndroidClassLoader.java @@ -0,0 +1,42 @@ +package io.anuke.mindustry.rhino; + +import android.annotation.*; +import android.os.*; +import com.android.dex.*; +import dalvik.system.*; +import io.anuke.arc.util.ArcAnnotate.NonNull; +import io.anuke.arc.util.ArcAnnotate.Nullable; + +import java.nio.*; + +/** + * @author F43nd1r + * @since 24.10.2017 + */ + +@TargetApi(Build.VERSION_CODES.O) +class InMemoryAndroidClassLoader extends BaseAndroidClassLoader{ + @Nullable + private Dex last; + + public InMemoryAndroidClassLoader(ClassLoader parent){ + super(parent); + } + + @Override + protected Class loadClass(@NonNull Dex dex, @NonNull String name) throws ClassNotFoundException{ + last = dex; + return new InMemoryDexClassLoader(ByteBuffer.wrap(dex.getBytes()), getParent()).loadClass(name); + } + + @Nullable + @Override + protected Dex getLastDex(){ + return last; + } + + @Override + protected void reset(){ + last = null; + } +} diff --git a/android/src/io/anuke/mindustry/rhino/RhinoBuilder.java b/android/src/io/anuke/mindustry/rhino/RhinoBuilder.java new file mode 100644 index 0000000000..b6df496945 --- /dev/null +++ b/android/src/io/anuke/mindustry/rhino/RhinoBuilder.java @@ -0,0 +1,83 @@ +package io.anuke.mindustry.rhino; + +import org.mozilla.javascript.*; + +import java.io.*; + +/** + * Helps to prepare a Rhino Context for usage on android. + * @author F43nd1r + * @since 11.01.2016 + */ +public class RhinoBuilder{ + private final File cacheDirectory; + + /** + * Constructs a new helper using the default temporary directory. + * Note: It is recommended to use a custom directory, so no permission problems occur. + */ + public RhinoBuilder(){ + this(new File(System.getProperty("java.io.tmpdir", "."), "classes")); + } + + /** + * Constructs a new helper using a directory in the applications cache. + * @param context any context + */ + public RhinoBuilder(android.content.Context context){ + this(new File(context.getCacheDir(), "classes")); + } + + /** + * Constructs a helper using the specified directory as cache. + * @param cacheDirectory the cache directory to use + */ + public RhinoBuilder(File cacheDirectory){ + this.cacheDirectory = cacheDirectory; + } + + /** + * call this instead of {@link Context#enter()} + * @return a context prepared for android + */ + public Context enterContext(){ + if(!SecurityController.hasGlobal()) + SecurityController.initGlobal(new SecurityController(){ + @Override + public GeneratedClassLoader createClassLoader(ClassLoader classLoader, Object o){ + return Context.getCurrentContext().createClassLoader(classLoader); + } + + @Override + public Object getDynamicSecurityDomain(Object o){ + return null; + } + }); + return getContextFactory().enterContext(); + } + + /** + * @return The Context factory which has to be used on android. + */ + public AndroidContextFactory getContextFactory(){ + AndroidContextFactory factory; + if(!ContextFactory.hasExplicitGlobal()){ + factory = new AndroidContextFactory(cacheDirectory); + ContextFactory.getGlobalSetter().setContextFactoryGlobal(factory); + }else if(!(ContextFactory.getGlobal() instanceof AndroidContextFactory)){ + throw new IllegalStateException("Cannot initialize factory for Android Rhino: There is already another factory"); + }else{ + factory = (AndroidContextFactory)ContextFactory.getGlobal(); + } + return factory; + } + + /** + * @return a context prepared for android + * @deprecated use {@link #enterContext()} instead + */ + @Deprecated + public static Context prepareContext(){ + return new RhinoBuilder().enterContext(); + } +} diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 2a32d86f22..a42c94141a 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -114,6 +114,7 @@ mod.author = [LIGHT_GRAY]Author:[] {0} mod.missing = This save contains mods that you have recently updated or no longer have installed. Save corruption may occur. Are you sure you want to load it?\n[lightgray]Mods:\n{0} mod.preview.missing = Before publishing this mod in the workshop, you must add an image preview.\nPlace an image named[accent] preview.png[] into the mod's folder and try again. mod.folder.missing = Only mods in folder form can be published on the workshop.\nTo convert any mod into a folder, simply unzip its file into a folder and delete the old zip, then restart your game or reload your mods. +mod.scripts.unsupported = Your device does not support mod scripts. Some mods will not function correctly. about.button = About name = Name: diff --git a/core/src/io/anuke/mindustry/mod/Mods.java b/core/src/io/anuke/mindustry/mod/Mods.java index bdc9d79027..0030d65721 100644 --- a/core/src/io/anuke/mindustry/mod/Mods.java +++ b/core/src/io/anuke/mindustry/mod/Mods.java @@ -381,6 +381,10 @@ public class Mods implements Loadable{ requiresReload = false; Events.fire(new ContentReloadEvent()); + + if(scripts != null && scripts.hasErrored()){ + Core.app.post(() -> Core.settings.getBoolOnce("scripts-errored", () -> ui.showErrorMessage("$mod.scripts.unsupported"))); + } } /** This must be run on the main thread! */ diff --git a/core/src/io/anuke/mindustry/mod/Scripts.java b/core/src/io/anuke/mindustry/mod/Scripts.java index 6ab13c4208..921e4b783e 100644 --- a/core/src/io/anuke/mindustry/mod/Scripts.java +++ b/core/src/io/anuke/mindustry/mod/Scripts.java @@ -12,20 +12,29 @@ public class Scripts implements Disposable{ private final Context context; private final String wrapper; private Scriptable scope; + private boolean errored; public Scripts(){ Time.mark(); context = Vars.platform.getScriptContext(); - context.setClassShutter(type -> (ClassAccess.allowedClassNames.contains(type) || type.startsWith("adapter") || type.contains("PrintStream") || type.startsWith("io.anuke.mindustry")) && !type.equals("io.anuke.mindustry.mod.ClassAccess")); + context.setClassShutter(type -> (ClassAccess.allowedClassNames.contains(type) || type.startsWith("$Proxy") || + type.startsWith("adapter") || type.contains("PrintStream") || + type.startsWith("io.anuke.mindustry")) && !type.equals("io.anuke.mindustry.mod.ClassAccess")); scope = new ImporterTopLevel(context); wrapper = Core.files.internal("scripts/wrapper.js").readString(); - run(Core.files.internal("scripts/global.js").readString(), "global.js"); + if(!run(Core.files.internal("scripts/global.js").readString(), "global.js")){ + errored = true; + } Log.debug("Time to load script engine: {0}", Time.elapsed()); } + public boolean hasErrored(){ + return errored; + } + public String runConsole(String text){ try{ Object o = context.evaluateString(scope, text, "console.js", 1, null); @@ -58,11 +67,13 @@ public class Scripts implements Disposable{ run(wrapper.replace("$SCRIPT_NAME$", mod.name + "/" + file.nameWithoutExtension()).replace("$CODE$", file.readString()).replace("$MOD_NAME$", mod.name), file.name()); } - private void run(String script, String file){ + private boolean run(String script, String file){ try{ context.evaluateString(scope, script, file, 1, null); + return true; }catch(Throwable t){ log(LogLevel.err, file, "" + getError(t)); + return false; } }