diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 1ac0e8b9..551458e0 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -96,6 +96,8 @@ dependencies { implementation 'com.moandjiezana.toml:toml4j:0.7.2' implementation 'org.ow2.asm:asm:9.7' + + implementation 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-depchain:3.3.2' } task(writeBuildClassJava) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java index d38c12fe..3c0e7327 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java @@ -1,221 +1,223 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util; - -import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; -import io.github.deltacv.common.image.MatPoster; -import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; -import org.opencv.core.Mat; -import org.openftc.easyopencv.MatRecycler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.concurrent.ArrayBlockingQueue; - -public class ThreadedMatPoster implements MatPoster { - private final ArrayList postables = new ArrayList<>(); - - private final EvictingBlockingQueue postQueue; - private final MatRecycler matRecycler; - - private final String name; - - private final Thread posterThread; - - public final FpsCounter fpsCounter = new FpsCounter(); - - private final Object lock = new Object(); - - private volatile boolean paused = false; - - private volatile boolean hasPosterThreadStarted = false; - - Logger logger; - - public static ThreadedMatPoster createWithoutRecycler(String name, int maxQueueItems) { - return new ThreadedMatPoster(name, maxQueueItems, null); - } - - public ThreadedMatPoster(String name, int maxQueueItems) { - this(name, new MatRecycler(maxQueueItems + 2)); - } - - public ThreadedMatPoster(String name, MatRecycler recycler) { - this(name, recycler.getSize(), recycler); - } - - public ThreadedMatPoster(String name, int maxQueueItems, MatRecycler recycler) { - postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); - matRecycler = recycler; - posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); - - this.name = name; - - logger = LoggerFactory.getLogger("MatPoster-" + name); - - postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue - } - - @Override - public void post(Mat m, Object context) { - if (m == null || m.empty()) { - logger.warn("Tried to post empty or null mat, skipped this frame."); - return; - } - - if (matRecycler != null) { - if(matRecycler.getAvailableMatsAmount() < 1) { - //evict one if we don't have any available mats in the recycler - evict(postQueue.poll()); - } - - MatRecycler.RecyclableMat recycledMat = matRecycler.takeMatOrNull(); - m.copyTo(recycledMat); - - postQueue.offer(recycledMat); - } else { - postQueue.offer(m); - } - } - - public void synchronizedPost(Mat m) { - synchronize(() -> post(m)); - } - - public Mat pull() throws InterruptedException { - synchronized(lock) { - return postQueue.take(); - } - } - - public void clearQueue() { - if(postQueue.size() == 0) return; - - synchronized(lock) { - postQueue.clear(); - } - } - - public void synchronize(Runnable runn) { - synchronized(lock) { - runn.run(); - } - } - - public void addPostable(Postable postable) { - //start mat posting thread if it hasn't been started yet - if (!posterThread.isAlive() && !hasPosterThreadStarted) { - posterThread.start(); - } - - postables.add(postable); - } - - public void stop() { - logger.info("Destroying..."); - - posterThread.interrupt(); - - for (Mat m : postQueue) { - if (m != null) { - if(m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat)m).returnMat(); - } - } - } - - matRecycler.releaseAll(); - } - - private void evict(Mat m) { - if (m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) m).returnMat(); - } - m.release(); - } - - public void setPaused(boolean paused) { - this.paused = paused; - } - - public boolean getPaused() { - synchronized(lock) { - return paused; - } - } - - public String getName() { - return name; - } - - public interface Postable { - void post(Mat m); - } - - private class PosterRunnable implements Runnable { - - private Mat postableMat = new Mat(); - - @Override - public void run() { - hasPosterThreadStarted = true; - - while (!Thread.interrupted()) { - - while(paused && !Thread.currentThread().isInterrupted()) { - Thread.yield(); - } - - if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames - - synchronized(lock) { - fpsCounter.update(); - - try { - Mat takenMat = postQueue.take(); - - for (Postable postable : postables) { - takenMat.copyTo(postableMat); - postable.post(postableMat); - } - - takenMat.release(); - - if (takenMat instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) takenMat).returnMat(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - break; - } catch (Exception ex) { } - } - - } - - logger.warn("Thread interrupted (" + Integer.toHexString(hashCode()) + ")"); - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util; + +import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; +import io.github.deltacv.common.image.MatPoster; +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; +import org.opencv.core.Mat; +import org.openftc.easyopencv.MatRecycler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; + +public class ThreadedMatPoster implements MatPoster { + private final ArrayList postables = new ArrayList<>(); + + private final EvictingBlockingQueue postQueue; + private final MatRecycler matRecycler; + + private final String name; + + private final Thread posterThread; + + public final FpsCounter fpsCounter = new FpsCounter(); + + private final Object lock = new Object(); + + private volatile boolean paused = false; + + private volatile boolean hasPosterThreadStarted = false; + + Logger logger; + + public static ThreadedMatPoster createWithoutRecycler(String name, int maxQueueItems) { + return new ThreadedMatPoster(name, maxQueueItems, null); + } + + public ThreadedMatPoster(String name, int maxQueueItems) { + this(name, new MatRecycler(maxQueueItems + 2)); + } + + public ThreadedMatPoster(String name, MatRecycler recycler) { + this(name, recycler.getSize(), recycler); + } + + public ThreadedMatPoster(String name, int maxQueueItems, MatRecycler recycler) { + postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); + matRecycler = recycler; + posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); + + this.name = name; + + logger = LoggerFactory.getLogger("MatPoster-" + name); + + postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue + } + + @Override + public void post(Mat m, Object context) { + if (m == null || m.empty()) { + logger.warn("Tried to post empty or null mat, skipped this frame."); + return; + } + + if (matRecycler != null) { + if(matRecycler.getAvailableMatsAmount() < 1) { + //evict one if we don't have any available mats in the recycler + evict(postQueue.poll()); + } + + MatRecycler.RecyclableMat recycledMat = matRecycler.takeMatOrNull(); + m.copyTo(recycledMat); + + postQueue.offer(recycledMat); + } else { + postQueue.offer(m); + } + } + + public void synchronizedPost(Mat m) { + synchronize(() -> post(m)); + } + + public Mat pull() throws InterruptedException { + synchronized(lock) { + return postQueue.take(); + } + } + + public void clearQueue() { + if(postQueue.size() == 0) return; + + synchronized(lock) { + postQueue.clear(); + } + } + + public void synchronize(Runnable runn) { + synchronized(lock) { + runn.run(); + } + } + + public void addPostable(Postable postable) { + //start mat posting thread if it hasn't been started yet + if (!posterThread.isAlive() && !hasPosterThreadStarted) { + posterThread.start(); + } + + postables.add(postable); + } + + public void stop() { + logger.info("Destroying..."); + + posterThread.interrupt(); + + for (Mat m : postQueue) { + if (m != null) { + if(m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat)m).returnMat(); + } + } + } + + matRecycler.releaseAll(); + } + + private void evict(Mat m) { + if (m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) m).returnMat(); + } + if(m != null) { + m.release(); + } + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean getPaused() { + synchronized(lock) { + return paused; + } + } + + public String getName() { + return name; + } + + public interface Postable { + void post(Mat m); + } + + private class PosterRunnable implements Runnable { + + private Mat postableMat = new Mat(); + + @Override + public void run() { + hasPosterThreadStarted = true; + + while (!Thread.interrupted()) { + + while(paused && !Thread.currentThread().isInterrupted()) { + Thread.yield(); + } + + if (postQueue.isEmpty() || postables.isEmpty()) continue; //skip if we have no queued frames + + synchronized(lock) { + fpsCounter.update(); + + try { + Mat takenMat = postQueue.take(); + + for (Postable postable : postables) { + takenMat.copyTo(postableMat); + postable.post(postableMat); + } + + takenMat.release(); + + if (takenMat instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) takenMat).returnMat(); + } + } catch (InterruptedException e) { + logger.warn("Thread interrupted ({})", Integer.toHexString(hashCode())); + break; + } catch (Exception ex) { } + } + + } + + logger.warn("Thread interrupted ({})", Integer.toHexString(hashCode())); + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java index f4d09510..3ff0bdd9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java @@ -89,12 +89,16 @@ public void updateGuiFieldValues() { @Override public void setFieldValue(int index, Object newValue) throws IllegalAccessException { try { + double value; + if(newValue instanceof String) { - scalar.val[index] = Double.parseDouble((String) newValue); + value = Double.parseDouble((String) newValue); } else { - scalar.val[index] = (double)newValue; + value = (double)newValue; } - } catch (Exception ex) { + + scalar.val[index] = value; + } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid number", ex); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt index 2fd8253b..9c08fa77 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt @@ -10,4 +10,6 @@ fun String.removeFromEnd(rem: String): String { return substring(0, length - rem.length).trim() } return trim() -} \ No newline at end of file +} + +val Any.hexString get() = Integer.toHexString(hashCode())!! \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt index 13a9e264..97888157 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt @@ -1,68 +1,76 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin - -import io.github.deltacv.eocvsim.plugin.loader.PluginContext - -/** - * Represents a plugin for EOCV-Sim - */ -abstract class EOCVSimPlugin { - - /** - * The context of the plugin, containing entry point objects - */ - val context get() = PluginContext.current(this) - - /** - * The EOCV-Sim instance, containing the main functionality of the program - */ - val eocvSim get() = context.eocvSim - - /** - * The virtual filesystem assigned to this plugin. - * With this filesystem, the plugin can access files in a sandboxed environment. - * Without SuperAccess, the plugin can only access files in its own virtual fs. - * @see SandboxFileSystem - */ - val fileSystem get() = context.fileSystem - - var enabled = false - internal set - - /** - * Called when the plugin is loaded by the PluginLoader - */ - abstract fun onLoad() - - /** - * Called when the plugin is enabled by the PluginLoader - */ - abstract fun onEnable() - - /* - * Called when the plugin is disabled by the PluginLoader - */ - abstract fun onDisable() +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin + +import io.github.deltacv.eocvsim.plugin.loader.PluginContext + +/** + * Represents a plugin for EOCV-Sim + */ +abstract class EOCVSimPlugin { + + /** + * The context of the plugin, containing entry point objects + */ + val context get() = PluginContext.current(this) + + /** + * The EOCV-Sim instance, containing the main functionality of the program + */ + val eocvSim get() = context.eocvSim + + /** + * The virtual filesystem assigned to this plugin. + * With this filesystem, the plugin can access files in a sandboxed environment. + * Without SuperAccess, the plugin can only access files in its own virtual fs. + * @see SandboxFileSystem + */ + val fileSystem get() = context.fileSystem + + /** + * The classpath of the plugin, additional to the JVM classpath + * This classpath is used to load classes from the plugin jar file + * and other dependencies that come from other jar files. + * @see io.github.deltacv.eocvsim.plugin.loader.PluginClassLoader + */ + val classpath get() = context.classpath + + var enabled = false + internal set + + /** + * Called when the plugin is loaded by the PluginLoader + */ + abstract fun onLoad() + + /** + * Called when the plugin is enabled by the PluginLoader + */ + abstract fun onEnable() + + /* + * Called when the plugin is disabled by the PluginLoader + */ + abstract fun onDisable() } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt index af498d85..ec1197dd 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt @@ -1,159 +1,240 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd -import io.github.deltacv.eocvsim.sandbox.restrictions.MethodCallByteCodeChecker -import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingMethodBlacklist -import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageBlacklist -import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageWhitelist -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.net.URL -import java.nio.file.FileSystems -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - -/** - * ClassLoader for loading classes from a plugin jar file - * @param pluginJar the jar file of the plugin - * @param pluginContext the plugin context - */ -class PluginClassLoader(private val pluginJar: File, val pluginContextProvider: () -> PluginContext) : ClassLoader() { - - private val zipFile = try { - ZipFile(pluginJar) - } catch(e: Exception) { - throw IOException("Failed to open plugin JAR file", e) - } - - private val loadedClasses = mutableMapOf>() - - init { - FileSystems.getDefault() - } - - private fun loadClass(entry: ZipEntry): Class<*> { - val name = entry.name.removeFromEnd(".class").replace('/', '.') - - zipFile.getInputStream(entry).use { inStream -> - ByteArrayOutputStream().use { outStream -> - SysUtil.copyStream(inStream, outStream) - val bytes = outStream.toByteArray() - - if(!pluginContextProvider().hasSuperAccess) - MethodCallByteCodeChecker(bytes, dynamicLoadingMethodBlacklist) - - val clazz = defineClass(name, bytes, 0, bytes.size) - loadedClasses[name] = clazz - - return clazz - } - } - } - - override fun findClass(name: String) = loadedClasses[name] ?: loadClass(name, false) - - /** - * Load a class from the plugin jar file - * @param name the name of the class to load - * @return the loaded class - * @throws IllegalAccessError if the class is blacklisted - */ - fun loadClassStrict(name: String): Class<*> { - if(!pluginContextProvider().hasSuperAccess) { - for (blacklistedPackage in dynamicLoadingPackageBlacklist) { - if (name.contains(blacklistedPackage)) { - throw IllegalAccessError("Plugins are blacklisted to use $name") - } - } - } - - return loadClass(zipFile.getEntry(name.replace('.', '/') + ".class") ?: throw ClassNotFoundException(name)) - } - - override fun loadClass(name: String, resolve: Boolean): Class<*> { - var clazz = loadedClasses[name] - - if(clazz == null) { - var inWhitelist = false - - for(whiteListedPackage in dynamicLoadingPackageWhitelist) { - if(name.contains(whiteListedPackage)) { - inWhitelist = true - break - } - } - - if(!inWhitelist && !pluginContextProvider().hasSuperAccess) { - throw IllegalAccessError("Plugins are not whitelisted to use $name") - } - - clazz = try { - Class.forName(name) - } catch (e: ClassNotFoundException) { - loadClassStrict(name) - } - - if(resolve) resolveClass(clazz) - } - - return clazz!! - } - - override fun getResourceAsStream(name: String): InputStream? { - val entry = zipFile.getEntry(name) - - if(entry != null) { - try { - return zipFile.getInputStream(entry) - } catch (e: IOException) { } - } - - return super.getResourceAsStream(name) - } - - override fun getResource(name: String): URL? { - // Try to find the resource inside the plugin JAR - val entry = zipFile.getEntry(name) - - if (entry != null) { - try { - // Construct a URL for the resource inside the plugin JAR - return URL("jar:file:${pluginJar.absolutePath}!/$name") - } catch (e: Exception) { - e.printStackTrace() - } - } - - // Fallback to the parent classloader if not found in the plugin JAR - return super.getResource(name) - } - - override fun toString() = "PluginClassLoader@\"${pluginJar.name}\"" - +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd +import io.github.deltacv.eocvsim.sandbox.restrictions.MethodCallByteCodeChecker +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingMethodBlacklist +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageBlacklist +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageWhitelist +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.URL +import java.nio.file.FileSystems +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +/** + * ClassLoader for loading classes from a plugin jar file + * @param pluginJar the jar file of the plugin + * @param pluginContext the plugin context + */ +class PluginClassLoader(private val pluginJar: File, val classpath: List, val pluginContextProvider: () -> PluginContext) : ClassLoader() { + + private val zipFile = try { + ZipFile(pluginJar) + } catch(e: Exception) { + throw IOException("Failed to open plugin JAR file", e) + } + + private val loadedClasses = mutableMapOf>() + + init { + FileSystems.getDefault() + } + + private fun loadClass(entry: ZipEntry, zipFile: ZipFile = this.zipFile): Class<*> { + val name = entry.name.removeFromEnd(".class").replace('/', '.') + + zipFile.getInputStream(entry).use { inStream -> + ByteArrayOutputStream().use { outStream -> + SysUtil.copyStream(inStream, outStream) + val bytes = outStream.toByteArray() + + if(!pluginContextProvider().hasSuperAccess) + MethodCallByteCodeChecker(bytes, dynamicLoadingMethodBlacklist) + + val clazz = defineClass(name, bytes, 0, bytes.size) + loadedClasses[name] = clazz + + return clazz + } + } + } + + override fun findClass(name: String) = loadedClasses[name] ?: loadClass(name, false) + + /** + * Load a class from the plugin jar file + * @param name the name of the class to load + * @return the loaded class + * @throws IllegalAccessError if the class is blacklisted + */ + fun loadClassStrict(name: String): Class<*> { + if(!pluginContextProvider().hasSuperAccess) { + for (blacklistedPackage in dynamicLoadingPackageBlacklist) { + if (name.contains(blacklistedPackage)) { + throw IllegalAccessError("Plugins are blacklisted to use $name") + } + } + } + + return loadClass(zipFile.getEntry(name.replace('.', '/') + ".class") ?: throw ClassNotFoundException(name)) + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + var clazz = loadedClasses[name] + + if(clazz == null) { + var inWhitelist = false + + for(whiteListedPackage in dynamicLoadingPackageWhitelist) { + if(name.contains(whiteListedPackage)) { + inWhitelist = true + break + } + } + + if(!inWhitelist && !pluginContextProvider().hasSuperAccess) { + throw IllegalAccessError("Plugins are not whitelisted to use $name") + } + + clazz = try { + Class.forName(name) + } catch (_: ClassNotFoundException) { + val classpathClass = classFromClasspath(name) + + if(classpathClass != null) { + return classpathClass + } + + loadClassStrict(name) + } + + if(resolve) resolveClass(clazz) + } + + return clazz!! + } + + override fun getResourceAsStream(name: String): InputStream? { + // Try to find the resource inside the classpath + resourceAsStreamFromClasspath(name)?.let { return it } + + val entry = zipFile.getEntry(name) + + if(entry != null) { + try { + return zipFile.getInputStream(entry) + } catch (e: IOException) { } + } + + return super.getResourceAsStream(name) + } + + override fun getResource(name: String): URL? { + resourceFromClasspath(name)?.let { return it } + + // Try to find the resource inside the plugin JAR + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + // Construct a URL for the resource inside the plugin JAR + return URL("jar:file:${pluginJar.absolutePath}!/$name") + } catch (e: Exception) { + } + } + + // Fallback to the parent classloader if not found in the plugin JAR + return super.getResource(name) + } + + /** + * Get a resource from the classpath specified in the constructor + */ + fun resourceAsStreamFromClasspath(name: String): InputStream? { + for (file in classpath) { + if(file == pluginJar) continue + + val zipFile = ZipFile(file) + + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + return zipFile.getInputStream(entry) + } catch (e: Exception) { + } + } + } + + return null + } + + /** + * Get a resource from the classpath specified in the constructor + */ + fun resourceFromClasspath(name: String): URL? { + for (file in classpath) { + if(file == pluginJar) continue + + val zipFile = ZipFile(file) + + try { + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + return URL("jar:file:${file.absolutePath}!/$name") + } catch (e: Exception) { + } + } + } finally { + zipFile.close() + } + } + + return null + } + + /** + * Load a class from the classpath specified in the constructor + */ + fun classFromClasspath(className: String): Class<*>? { + for (file in classpath) { + if(file == pluginJar) continue + + val zipFile = ZipFile(file) + + try { + val entry = zipFile.getEntry(className.replace('.', '/') + ".class") + + if (entry != null) { + return loadClass(entry, zipFile = zipFile) + } + } finally { + zipFile.close() + } + } + + return null + } + + override fun toString() = "PluginClassLoader@\"${pluginJar.name}\"" + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt index 6fe21731..641b7d22 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt @@ -1,41 +1,42 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.EOCVSim -import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin -import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem - -class PluginContext( - val eocvSim: EOCVSim, val fileSystem: SandboxFileSystem, val loader: PluginLoader -) { - companion object { - @JvmStatic fun current(plugin: EOCVSimPlugin) = (plugin.javaClass.classLoader as PluginClassLoader).pluginContextProvider() - } - - val plugin get() = loader.plugin - - val hasSuperAccess get() = loader.hasSuperAccess - fun requestSuperAccess(reason: String) = loader.requestSuperAccess(reason) +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.EOCVSim +import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin +import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem + +class PluginContext( + val eocvSim: EOCVSim, val fileSystem: SandboxFileSystem, val loader: PluginLoader +) { + companion object { + @JvmStatic fun current(plugin: EOCVSimPlugin) = (plugin.javaClass.classLoader as PluginClassLoader).pluginContextProvider() + } + + val plugin get() = loader.plugin + val classpath get() = loader.classpath + + val hasSuperAccess get() = loader.hasSuperAccess + fun requestSuperAccess(reason: String) = loader.requestSuperAccess(reason) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt index c47ebafe..11375fc4 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt @@ -1,215 +1,219 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.config.ConfigLoader -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.extension.plus -import com.github.serivesmejia.eocvsim.util.loggerForThis -import com.moandjiezana.toml.Toml -import io.github.deltacv.common.util.ParsedVersion -import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin -import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem -import net.lingala.zip4j.ZipFile -import java.io.File -import java.security.MessageDigest - -/** - * Loads a plugin from a jar file - * @param pluginFile the jar file of the plugin - * @param eocvSim the EOCV-Sim instance - */ -class PluginLoader(val pluginFile: File, val eocvSim: EOCVSim) { - - val logger by loggerForThis() - - var loaded = false - private set - - var enabled = false - private set - - val pluginClassLoader: PluginClassLoader - - lateinit var pluginToml: Toml - private set - - lateinit var pluginName: String - private set - lateinit var pluginVersion: String - private set - - lateinit var pluginAuthor: String - private set - lateinit var pluginAuthorEmail: String - private set - - lateinit var pluginClass: Class<*> - private set - lateinit var plugin: EOCVSimPlugin - private set - - /** - * The file system for the plugin - */ - lateinit var fileSystem: SandboxFileSystem - private set - - val fileSystemZip by lazy { PluginManager.FILESYSTEMS_FOLDER + File.separator + "${hash()}-fs" } - val fileSystemZipPath by lazy { fileSystemZip.toPath() } - - /** - * Whether the plugin has super access (full system access) - */ - val hasSuperAccess get() = eocvSim.config.superAccessPluginHashes.contains(pluginHash) - - init { - pluginClassLoader = PluginClassLoader(pluginFile) { - PluginContext(eocvSim, fileSystem, this) - } - } - - /** - * Load the plugin from the jar file - * @throws InvalidPluginException if the plugin.toml file is not found - * @throws UnsupportedPluginException if the plugin requests an api version higher than the current one - */ - fun load() { - if(loaded) return - - pluginToml = Toml().read(pluginClassLoader.getResourceAsStream("plugin.toml") - ?: throw InvalidPluginException("No plugin.toml in the jar file") - ) - - pluginName = pluginToml.getString("name") ?: throw InvalidPluginException("No name in plugin.toml") - pluginVersion = pluginToml.getString("version") ?: throw InvalidPluginException("No version in plugin.toml") - - pluginAuthor = pluginToml.getString("author") ?: throw InvalidPluginException("No author in plugin.toml") - pluginAuthorEmail = pluginToml.getString("author-email", "") - - logger.info("Loading plugin $pluginName v$pluginVersion by $pluginAuthor") - - setupFs() - - if(pluginToml.contains("api-version")) { - val parsedVersion = ParsedVersion(pluginToml.getString("api-version")) - - if(parsedVersion > EOCVSim.PARSED_VERSION) - throw UnsupportedPluginException("Plugin request api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") - } - - if(pluginToml.getBoolean("super-access", false)) { - requestSuperAccess(pluginToml.getString("super-access-reason", "")) - } - - pluginClass = pluginClassLoader.loadClassStrict(pluginToml.getString("main")) - plugin = pluginClass.getConstructor().newInstance() as EOCVSimPlugin - - plugin.onLoad() - - loaded = true - } - - private fun setupFs() { - if(!fileSystemZip.exists()) { - val zip = ZipFile(fileSystemZip) // kinda wack but uh, yeah... - zip.addFile(ConfigLoader.CONFIG_SAVEFILE) - zip.removeFile(ConfigLoader.CONFIG_SAVEFILE.name) - zip.close() - } - - fileSystem = SandboxFileSystem(this) - } - - /** - * Enable the plugin - */ - fun enable() { - if(enabled || !loaded) return - - logger.info("Enabling plugin $pluginName v$pluginVersion") - - plugin.enabled = true - plugin.onEnable() - - enabled = true - } - - /** - * Disable the plugin - */ - fun disable() { - if(!enabled || !loaded) return - - logger.info("Disabling plugin $pluginName v$pluginVersion") - - plugin.enabled = false - plugin.onDisable() - - kill() - } - - /** - * Kill the plugin - * This will close the file system and ban the class loader - * @see EventHandler.banClassLoader - */ - fun kill() { - fileSystem.close() - enabled = false - EventHandler.banClassLoader(pluginClassLoader) - } - - /** - * Request super access for the plugin - * @param reason the reason for requesting super access - */ - fun requestSuperAccess(reason: String): Boolean { - if(hasSuperAccess) return true - return eocvSim.pluginManager.requestSuperAccessFor(this, reason) - } - - /** - * Get the hash of the plugin file based off the plugin name and author - * @return the hash - */ - fun hash(): String { - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update("${pluginName} by ${pluginAuthor}".toByteArray()) - return SysUtil.byteArray2Hex(messageDigest.digest()) - } - - /** - * Get the hash of the plugin file based off the file contents - * @return the hash - */ - val pluginHash by lazy { - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(pluginFile.readBytes()) - SysUtil.byteArray2Hex(messageDigest.digest()) - } - +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.config.ConfigLoader +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import io.github.deltacv.common.util.ParsedVersion +import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin +import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem +import net.lingala.zip4j.ZipFile +import java.io.File +import java.security.MessageDigest + +/** + * Loads a plugin from a jar file + * @param pluginFile the jar file of the plugin + * @param eocvSim the EOCV-Sim instance + */ +class PluginLoader( + val pluginFile: File, + val classpath: List, + val eocvSim: EOCVSim +) { + + val logger by loggerForThis() + + var loaded = false + private set + + var enabled = false + private set + + val pluginClassLoader: PluginClassLoader + + lateinit var pluginToml: Toml + private set + + lateinit var pluginName: String + private set + lateinit var pluginVersion: String + private set + + lateinit var pluginAuthor: String + private set + lateinit var pluginAuthorEmail: String + private set + + lateinit var pluginClass: Class<*> + private set + lateinit var plugin: EOCVSimPlugin + private set + + /** + * The file system for the plugin + */ + lateinit var fileSystem: SandboxFileSystem + private set + + val fileSystemZip by lazy { PluginManager.FILESYSTEMS_FOLDER + File.separator + "${hash()}-fs" } + val fileSystemZipPath by lazy { fileSystemZip.toPath() } + + /** + * Whether the plugin has super access (full system access) + */ + val hasSuperAccess get() = eocvSim.config.superAccessPluginHashes.contains(pluginHash) + + init { + pluginClassLoader = PluginClassLoader(pluginFile, classpath) { + PluginContext(eocvSim, fileSystem, this) + } + } + + /** + * Load the plugin from the jar file + * @throws InvalidPluginException if the plugin.toml file is not found + * @throws UnsupportedPluginException if the plugin requests an api version higher than the current one + */ + fun load() { + if(loaded) return + + pluginToml = Toml().read(pluginClassLoader.getResourceAsStream("plugin.toml") + ?: throw InvalidPluginException("No plugin.toml in the jar file") + ) + + pluginName = pluginToml.getString("name") ?: throw InvalidPluginException("No name in plugin.toml") + pluginVersion = pluginToml.getString("version") ?: throw InvalidPluginException("No version in plugin.toml") + + pluginAuthor = pluginToml.getString("author") ?: throw InvalidPluginException("No author in plugin.toml") + pluginAuthorEmail = pluginToml.getString("author-email", "") + + logger.info("Loading plugin $pluginName v$pluginVersion by $pluginAuthor") + + setupFs() + + if(pluginToml.contains("api-version")) { + val parsedVersion = ParsedVersion(pluginToml.getString("api-version")) + + if(parsedVersion > EOCVSim.PARSED_VERSION) + throw UnsupportedPluginException("Plugin request api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + } + + if(pluginToml.getBoolean("super-access", false)) { + requestSuperAccess(pluginToml.getString("super-access-reason", "")) + } + + pluginClass = pluginClassLoader.loadClassStrict(pluginToml.getString("main")) + plugin = pluginClass.getConstructor().newInstance() as EOCVSimPlugin + + plugin.onLoad() + + loaded = true + } + + private fun setupFs() { + if(!fileSystemZip.exists()) { + val zip = ZipFile(fileSystemZip) // kinda wack but uh, yeah... + zip.addFile(ConfigLoader.CONFIG_SAVEFILE) + zip.removeFile(ConfigLoader.CONFIG_SAVEFILE.name) + zip.close() + } + + fileSystem = SandboxFileSystem(this) + } + + /** + * Enable the plugin + */ + fun enable() { + if(enabled || !loaded) return + + logger.info("Enabling plugin $pluginName v$pluginVersion") + + plugin.enabled = true + plugin.onEnable() + + enabled = true + } + + /** + * Disable the plugin + */ + fun disable() { + if(!enabled || !loaded) return + + logger.info("Disabling plugin $pluginName v$pluginVersion") + + plugin.enabled = false + plugin.onDisable() + + kill() + } + + /** + * Kill the plugin + * This will close the file system and ban the class loader + * @see EventHandler.banClassLoader + */ + fun kill() { + fileSystem.close() + enabled = false + EventHandler.banClassLoader(pluginClassLoader) + } + + /** + * Request super access for the plugin + * @param reason the reason for requesting super access + */ + fun requestSuperAccess(reason: String): Boolean { + if(hasSuperAccess) return true + return eocvSim.pluginManager.requestSuperAccessFor(this, reason) + } + + /** + * Get the hash of the plugin file based off the plugin name and author + * @return the hash + */ + fun hash(): String { + val messageDigest = MessageDigest.getInstance("SHA-256") + messageDigest.update("${pluginName} by ${pluginAuthor}".toByteArray()) + return SysUtil.byteArray2Hex(messageDigest.digest()) + } + + /** + * Get the hash of the plugin file based off the file contents + * @return the hash + */ + val pluginHash by lazy { + val messageDigest = MessageDigest.getInstance("SHA-256") + messageDigest.update(pluginFile.readBytes()) + SysUtil.byteArray2Hex(messageDigest.digest()) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt index ff137677..78aee988 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt @@ -1,170 +1,180 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.util.JavaProcess -import com.github.serivesmejia.eocvsim.util.extension.plus -import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder -import com.github.serivesmejia.eocvsim.util.loggerForThis -import io.github.deltacv.eocvsim.gui.dialog.SuperAccessRequestMain -import java.io.File -import java.util.* - -/** - * Manages the loading, enabling and disabling of plugins - * @param eocvSim the EOCV-Sim instance - */ -class PluginManager(val eocvSim: EOCVSim) { - - companion object { - val PLUGIN_FOLDER = (EOCVSimFolder + File.separator + "plugins").apply { mkdir() } - val FILESYSTEMS_FOLDER = (PLUGIN_FOLDER + File.separator + "filesystems").apply { mkdir() } - - const val GENERIC_SUPERACCESS_WARN = "Plugins run in a restricted environment by default. SuperAccess will grant full system access. Ensure you trust the authors before accepting." - const val GENERIC_LAWYER_YEET = "

By accepting, you acknowledge that deltacv is not responsible for damages caused by third-party plugins." - } - - val logger by loggerForThis() - - private val _pluginFiles = mutableListOf() - - /** - * List of plugin files in the plugins folder - */ - val pluginFiles get() = _pluginFiles.toList() - - private val loaders = mutableMapOf() - - private var isEnabled = false - - /** - * Initializes the plugin manager - * Loads all plugin files in the plugins folder - * Creates a PluginLoader for each plugin file - * and stores them in the loaders map - * @see PluginLoader - */ - fun init() { - val filesInPluginFolder = PLUGIN_FOLDER.listFiles() ?: arrayOf() - - for (file in filesInPluginFolder) { - if (file.extension == "jar") _pluginFiles.add(file) - } - - if(pluginFiles.isEmpty()) { - logger.info("No plugins to load") - return - } - - for (pluginFile in pluginFiles) { - loaders[pluginFile] = PluginLoader(pluginFile, eocvSim) - } - - isEnabled = true - } - - /** - * Loads all plugins - * @see PluginLoader.load - */ - fun loadPlugins() { - for ((file, loader) in loaders) { - try { - loader.load() - } catch (e: Throwable) { - logger.error("Failure loading ${file.name}", e) - loaders.remove(file) - loader.kill() - } - } - } - - /** - * Enables all plugins - * @see PluginLoader.enable - */ - fun enablePlugins() { - for (loader in loaders.values) { - try { - loader.enable() - } catch (e: Throwable) { - logger.error("Failure enabling ${loader.pluginName} v${loader.pluginVersion}", e) - loader.kill() - } - } - } - - /** - * Disables all plugins - * @see PluginLoader.disable - */ - @Synchronized - fun disablePlugins() { - if(!isEnabled) return - - for (loader in loaders.values) { - try { - loader.disable() - } catch (e: Throwable) { - logger.error("Failure disabling ${loader.pluginName} v${loader.pluginVersion}", e) - loader.kill() - } - } - - isEnabled = false - } - - /** - * Requests super access for a plugin loader - * - * @param loader the plugin loader to request super access for - * @param reason the reason for requesting super access - * @return true if super access was granted, false otherwise - */ - fun requestSuperAccessFor(loader: PluginLoader, reason: String): Boolean { - if(loader.hasSuperAccess) return true - - logger.info("Requesting super access for ${loader.pluginName} v${loader.pluginVersion}") - - var warning = "$GENERIC_SUPERACCESS_WARN" - if(reason.trim().isNotBlank()) { - warning += "

$reason" - } - - warning += GENERIC_LAWYER_YEET - - warning += "" - - val name = "${loader.pluginName} by ${loader.pluginAuthor}".replace(" ", "-") - - if(JavaProcess.exec(SuperAccessRequestMain::class.java, null, Arrays.asList(name, warning)) == 171) { - eocvSim.config.superAccessPluginHashes.add(loader.pluginHash) - eocvSim.configManager.saveToFile() - return true - } - - return false - } +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.util.JavaProcess +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder +import com.github.serivesmejia.eocvsim.util.loggerForThis +import io.github.deltacv.eocvsim.gui.dialog.SuperAccessRequestMain +import io.github.deltacv.eocvsim.plugin.repository.PluginRepositoryManager +import java.io.File +import java.util.* + +/** + * Manages the loading, enabling and disabling of plugins + * @param eocvSim the EOCV-Sim instance + */ +class PluginManager(val eocvSim: EOCVSim) { + + companion object { + val PLUGIN_FOLDER = (EOCVSimFolder + File.separator + "plugins").apply { mkdir() } + val FILESYSTEMS_FOLDER = (PLUGIN_FOLDER + File.separator + "filesystems").apply { mkdir() } + + const val GENERIC_SUPERACCESS_WARN = "Plugins run in a restricted environment by default. SuperAccess will grant full system access. Ensure you trust the authors before accepting." + const val GENERIC_LAWYER_YEET = "

By accepting, you acknowledge that deltacv is not responsible for damages caused by third-party plugins." + } + + val logger by loggerForThis() + + val repositoryManager = PluginRepositoryManager() + + private val _pluginFiles = mutableListOf() + + /** + * List of plugin files in the plugins folder + */ + val pluginFiles get() = _pluginFiles.toList() + + private val loaders = mutableMapOf() + + private var isEnabled = false + + /** + * Initializes the plugin manager + * Loads all plugin files in the plugins folder + * Creates a PluginLoader for each plugin file + * and stores them in the loaders map + * @see PluginLoader + */ + fun init() { + repositoryManager.init() + + val pluginFiles = mutableListOf() + pluginFiles.addAll(repositoryManager.resolveAll()) + + PLUGIN_FOLDER.listFiles()?.let { + pluginFiles.addAll(it.filter { it.extension == "jar" }) + } + + for (file in pluginFiles) { + if (file.extension == "jar") _pluginFiles.add(file) + } + + if(pluginFiles.isEmpty()) { + logger.info("No plugins to load") + return + } + + for (pluginFile in pluginFiles) { + loaders[pluginFile] = PluginLoader(pluginFile, repositoryManager.classpath(), eocvSim) + } + + isEnabled = true + } + + /** + * Loads all plugins + * @see PluginLoader.load + */ + fun loadPlugins() { + for ((file, loader) in loaders) { + try { + loader.load() + } catch (e: Throwable) { + logger.error("Failure loading ${file.name}", e) + loaders.remove(file) + loader.kill() + } + } + } + + /** + * Enables all plugins + * @see PluginLoader.enable + */ + fun enablePlugins() { + for (loader in loaders.values) { + try { + loader.enable() + } catch (e: Throwable) { + logger.error("Failure enabling ${loader.pluginName} v${loader.pluginVersion}", e) + loader.kill() + } + } + } + + /** + * Disables all plugins + * @see PluginLoader.disable + */ + @Synchronized + fun disablePlugins() { + if(!isEnabled) return + + for (loader in loaders.values) { + try { + loader.disable() + } catch (e: Throwable) { + logger.error("Failure disabling ${loader.pluginName} v${loader.pluginVersion}", e) + loader.kill() + } + } + + isEnabled = false + } + + /** + * Requests super access for a plugin loader + * + * @param loader the plugin loader to request super access for + * @param reason the reason for requesting super access + * @return true if super access was granted, false otherwise + */ + fun requestSuperAccessFor(loader: PluginLoader, reason: String): Boolean { + if(loader.hasSuperAccess) return true + + logger.info("Requesting super access for ${loader.pluginName} v${loader.pluginVersion}") + + var warning = "$GENERIC_SUPERACCESS_WARN" + if(reason.trim().isNotBlank()) { + warning += "

$reason" + } + + warning += GENERIC_LAWYER_YEET + + warning += "" + + val name = "${loader.pluginName} by ${loader.pluginAuthor}".replace(" ", "-") + + if(JavaProcess.exec(SuperAccessRequestMain::class.java, null, Arrays.asList(name, warning)) == 171) { + eocvSim.config.superAccessPluginHashes.add(loader.pluginHash) + eocvSim.configManager.saveToFile() + return true + } + + return false + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt new file mode 100644 index 00000000..765b480a --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt @@ -0,0 +1,144 @@ +package io.github.deltacv.eocvsim.plugin.repository + +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.hexString +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import com.moandjiezana.toml.TomlWriter +import io.github.deltacv.eocvsim.plugin.loader.PluginManager +import org.jboss.shrinkwrap.resolver.api.maven.ConfigurableMavenResolverSystem +import org.jboss.shrinkwrap.resolver.api.maven.Maven +import java.io.File + +class PluginRepositoryManager { + + companion object { + val REPOSITORY_FILE = PluginManager.PLUGIN_FOLDER + File.separator + "repository.toml" + val CACHE_FILE = PluginManager.PLUGIN_FOLDER + File.separator + "cache.toml" + + val MAVEN_FOLDER = PluginManager.PLUGIN_FOLDER + File.separator + "maven" + + val REPOSITORY_TOML_RES = PluginRepositoryManager::class.java.getResourceAsStream("/repository.toml") + val CACHE_TOML_RES = PluginRepositoryManager::class.java.getResourceAsStream("/cache.toml") + + const val UNSURE_USER_HELP = "If you're unsure on what to do, delete the repository.toml file and restart the program." + } + + private lateinit var pluginsToml: Toml + private lateinit var pluginsRepositories: Toml + private lateinit var plugins: Toml + + private lateinit var cacheToml: Toml + + private lateinit var resolver: ConfigurableMavenResolverSystem + + val logger by loggerForThis() + + fun init() { + logger.info("Initializing plugin repository manager") + + SysUtil.copyFileIs(CACHE_TOML_RES, CACHE_FILE, false) + cacheToml = Toml().read(CACHE_FILE) + + MAVEN_FOLDER.mkdir() + + SysUtil.copyFileIs(REPOSITORY_TOML_RES, REPOSITORY_FILE, false) + pluginsToml = Toml().read(REPOSITORY_FILE) + + pluginsRepositories = pluginsToml.getTable("repositories") + ?: throw InvalidFileException("No repositories found in repository.toml. $UNSURE_USER_HELP") + plugins = pluginsToml.getTable("plugins") + ?: Toml() + + resolver = Maven.configureResolver() + + for (repo in pluginsRepositories.toMap()) { + if(repo.value !is String) + throw InvalidFileException("Invalid repository URL in repository.toml. $UNSURE_USER_HELP") + + resolver.withRemoteRepo(repo.key, repo.value as String, "default") + + logger.info("Added repository ${repo.key} with URL ${repo.value}") + } + } + + fun resolveAll(): List { + val files = mutableListOf() + + val newCache = mutableMapOf() + + for(cache in cacheToml.toMap()) { + newCache[cache.key] = cache.value as String + } + + for(plugin in plugins.toMap()) { + if(plugin.value !is String) + throw InvalidFileException("Invalid plugin dependency in repository.toml. $UNSURE_USER_HELP") + + logger.info("Resolving plugin dependency ${plugin.key} with ${plugin.value}") + + val pluginDep = plugin.value as String + + var pluginJar: File? = null + + try { + for(cached in cacheToml.toMap()) { + if(cached.key == pluginDep.hexString) { + logger.info("Found cached plugin dependency $pluginDep (${pluginDep.hexString})") + + val cachedFile = File(cached.value as String) + if(cachedFile.exists()) { + files += cachedFile + continue + } + } + } + + resolver.resolve(pluginDep) + .withTransitivity() + .asFile() + .forEach { file -> + if(pluginJar == null) { + // the first file is the plugin jar + pluginJar = file + } + + val destFile = File(MAVEN_FOLDER, file.name) + file.copyTo(destFile, true) + + newCache[pluginDep.hexString] = pluginJar!!.absolutePath + } + + files += pluginJar!! + } catch(ex: Exception) { + logger.warn("Failed to resolve plugin dependency $pluginDep", ex) + } + } + + val cacheBuilder = StringBuilder() + cacheBuilder.append("# Do not edit this file, it is generated by the application.\n") + for(cached in newCache) { + cacheBuilder.append("${cached.key} = \"${cached.value.replace("\\", "/")}\"\n") + } + + SysUtil.saveFileStr(CACHE_FILE, cacheBuilder.toString()) + + return files + } + + fun classpath(): List { + val files = mutableListOf() + + for(jar in MAVEN_FOLDER.listFiles() ?: arrayOf()) { + if(jar.extension == "jar") { + files += jar + } + } + + return files + } +} + + +class InvalidFileException(msg: String) : RuntimeException(msg) \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/cache.toml b/EOCV-Sim/src/main/resources/cache.toml new file mode 100644 index 00000000..92466bcc --- /dev/null +++ b/EOCV-Sim/src/main/resources/cache.toml @@ -0,0 +1 @@ +# Do not edit this file, it is generated by the application. \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/repository.toml b/EOCV-Sim/src/main/resources/repository.toml new file mode 100644 index 00000000..4091f87d --- /dev/null +++ b/EOCV-Sim/src/main/resources/repository.toml @@ -0,0 +1,6 @@ +[repositories] +central = "https://repo.maven.apache.org/maven2/" +jitpack = "https://jitpack.io/" + +[plugins] +PaperVision = "com.github.deltacv:PaperVision:1.0.0" \ No newline at end of file diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index a316f0a9..264ec071 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -1,5 +1,3 @@ -import java.nio.file.Paths - plugins { id 'org.jetbrains.kotlin.jvm' } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java index 5e1271fa..7302ad47 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java @@ -25,14 +25,7 @@ import org.firstinspires.ftc.robotcore.external.Telemetry; import org.firstinspires.ftc.robotcore.external.navigation.*; import org.opencv.calib3d.Calib3d; -import org.opencv.core.CvType; -import org.opencv.core.Mat; -import org.opencv.core.MatOfDouble; -import org.opencv.core.MatOfPoint2f; -import org.opencv.core.MatOfPoint3f; -import org.opencv.core.Point; -import org.opencv.core.Point3; -import org.opencv.core.Scalar; +import org.opencv.core.*; import org.opencv.imgproc.Imgproc; import org.openftc.apriltag.AprilTagDetection; import org.openftc.apriltag.AprilTagDetectorJNI; diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java index 66abc6a0..61fadca5 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java @@ -1,359 +1,359 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import com.qualcomm.robotcore.util.ElapsedTime; -import com.qualcomm.robotcore.util.MovingStatistics; -import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator; -import io.github.deltacv.vision.external.PipelineRenderHook; -import org.opencv.android.Utils; -import org.opencv.core.*; -import org.opencv.imgproc.Imgproc; - -public abstract class OpenCvCameraBase implements OpenCvCamera { - - private OpenCvPipeline pipeline = null; - - private OpenCvViewport viewport; - private OpenCvCameraRotation rotation; - - private final Object pipelineChangeLock = new Object(); - - private Mat rotatedMat = new Mat(); - private Mat matToUseIfPipelineReturnedCropped; - private Mat croppedColorCvtedMat = new Mat(); - - private boolean isStreaming = false; - private boolean viewportEnabled = true; - - private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE; - ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; - boolean fpsMeterDesired = true; - - private Scalar brown = new Scalar(82, 61, 46, 255); - - private int frameCount = 0; - - private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator(); - - private double width; - private double height; - - public OpenCvCameraBase(OpenCvViewport viewport, boolean viewportEnabled) { - this.viewport = viewport; - this.rotation = getDefaultRotation(); - - this.viewportEnabled = viewportEnabled; - } - - @Override - public void showFpsMeterOnViewport(boolean show) { - viewport.setFpsMeterEnabled(show); - } - - @Override - public void pauseViewport() { - viewport.pause(); - } - - @Override - public void resumeViewport() { - viewport.resume(); - } - - @Override - public void setViewportRenderingPolicy(ViewportRenderingPolicy policy) { - viewport.setRenderingPolicy(policy); - } - - @Override - public void setViewportRenderer(ViewportRenderer renderer) { - this.desiredViewportRenderer = renderer; - } - - @Override - public void setPipeline(OpenCvPipeline pipeline) { - this.pipeline = pipeline; - } - - @Override - public int getFrameCount() { - return frameCount; - } - - @Override - public float getFps() { - return statistics.getAvgFps(); - } - - @Override - public int getPipelineTimeMs() { - return statistics.getAvgPipelineTime(); - } - - @Override - public int getOverheadTimeMs() { - return statistics.getAvgOverheadTime(); - } - - @Override - public int getTotalFrameTimeMs() { - return getTotalFrameTimeMs(); - } - - @Override - public int getCurrentPipelineMaxFps() { - return 0; - } - - @Override - public void startRecordingPipeline(PipelineRecordingParameters parameters) { - } - - @Override - public void stopRecordingPipeline() { - } - - protected void notifyStartOfFrameProcessing() { - statistics.newInputFrameStart(); - } - - public synchronized final void prepareForOpenCameraDevice() - { - if (viewportEnabled) - { - setupViewport(); - viewport.setRenderingPolicy(desiredRenderingPolicy); - } - } - - public synchronized final void prepareForStartStreaming(int width, int height, OpenCvCameraRotation rotation) - { - this.rotation = rotation; - this.statistics = new PipelineStatisticsCalculator(); - this.statistics.init(); - - Size sizeAfterRotation = getFrameSizeAfterRotation(width, height, rotation); - - this.width = sizeAfterRotation.width; - this.height = sizeAfterRotation.height; - - if(viewport != null) - { - // viewport.setSize(width, height); - viewport.setOptimizedViewRotation(getOptimizedViewportRotation(rotation)); - viewport.activate(); - } - } - - public synchronized final void cleanupForEndStreaming() { - matToUseIfPipelineReturnedCropped = null; - - if (viewport != null) { - viewport.deactivate(); - } - } - - protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { - statistics.newPipelineFrameStart(); - - Mat userProcessedFrame = null; - - int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation); - - if (rotateCode != -1) { - /* - * Rotate onto another Mat rather than doing so in-place. - * - * This does two things: - * 1) It seems that rotating by 90 or 270 in-place - * causes the backing buffer to be re-allocated - * since the width/height becomes swapped. This - * causes a problem for user code which makes a - * submat from the input Mat, because after the - * parent Mat is re-allocated the submat is no - * longer tied to it. Thus, by rotating onto - * another Mat (which is never re-allocated) we - * remove that issue. - * - * 2) Since the backing buffer does need need to be - * re-allocated for each frame, we reduce overhead - * time by about 1ms. - */ - Core.rotate(frame, rotatedMat, rotateCode); - frame = rotatedMat; - } - - final OpenCvPipeline pipelineSafe; - - // Grab a safe reference to what the pipeline currently is, - // since the user is allowed to change it at any time - synchronized (pipelineChangeLock) { - pipelineSafe = pipeline; - } - - if (pipelineSafe != null) { - if (pipelineSafe instanceof TimestampedOpenCvPipeline) { - ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp); - } - - statistics.beforeProcessFrame(); - - userProcessedFrame = pipelineSafe.processFrameInternal(frame); - - statistics.afterProcessFrame(); - } - - // Will point to whatever mat we end up deciding to send to the screen - final Mat matForDisplay; - - if (pipelineSafe == null) { - matForDisplay = frame; - } else if (userProcessedFrame == null) { - throw new OpenCvCameraException("User pipeline returned null"); - } else if (userProcessedFrame.empty()) { - throw new OpenCvCameraException("User pipeline returned empty mat"); - } else if (userProcessedFrame.cols() != frame.cols() || userProcessedFrame.rows() != frame.rows()) { - /* - * The user didn't return the same size image from their pipeline as we gave them, - * ugh. This makes our lives interesting because we can't just send an arbitrary - * frame size to the viewport. It re-uses framebuffers that are of a fixed resolution. - * So, we copy the user's Mat onto a Mat of the correct size, and then send that other - * Mat to the viewport. - */ - - if (userProcessedFrame.cols() > frame.cols() || userProcessedFrame.rows() > frame.rows()) { - /* - * What on earth was this user thinking?! They returned a Mat that's BIGGER in - * a dimension than the one we gave them! - */ - - throw new OpenCvCameraException("User pipeline returned frame of unexpected size"); - } - - //We re-use this buffer, only create if needed - if (matToUseIfPipelineReturnedCropped == null) { - matToUseIfPipelineReturnedCropped = frame.clone(); - } - - //Set to brown to indicate to the user the areas which they cropped off - matToUseIfPipelineReturnedCropped.setTo(brown); - - int usrFrmTyp = userProcessedFrame.type(); - - if (usrFrmTyp == CvType.CV_8UC1) { - /* - * Handle 8UC1 returns (masks and single channels of images); - * - * We have to color convert onto a different mat (rather than - * doing so in place) to avoid breaking any of the user's submats - */ - Imgproc.cvtColor(userProcessedFrame, croppedColorCvtedMat, Imgproc.COLOR_GRAY2RGBA); - userProcessedFrame = croppedColorCvtedMat; //Doesn't affect user's handle, only ours - } else if (usrFrmTyp != CvType.CV_8UC4 && usrFrmTyp != CvType.CV_8UC3) { - /* - * Oof, we don't know how to handle the type they gave us - */ - throw new OpenCvCameraException("User pipeline returned a frame of an illegal type. Valid types are CV_8UC1, CV_8UC3, and CV_8UC4"); - } - - //Copy the user's frame onto a Mat of the correct size - userProcessedFrame.copyTo(matToUseIfPipelineReturnedCropped.submat( - new Rect(0, 0, userProcessedFrame.cols(), userProcessedFrame.rows()))); - - //Send that correct size Mat to the viewport - matForDisplay = matToUseIfPipelineReturnedCropped; - } else { - /* - * Yay, smart user! They gave us the frame size we were expecting! - * Go ahead and send it right on over to the viewport. - */ - matForDisplay = userProcessedFrame; - } - - if (viewport != null) { - viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null)); - } - - statistics.endFrame(); - - if (viewport != null) { - viewport.notifyStatistics(statistics.getAvgFps(), statistics.getAvgPipelineTime(), statistics.getAvgOverheadTime()); - } - - frameCount++; - } - - - protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCameraRotation streamRotation) { - if (!cameraOrientationIsTiedToDeviceOrientation()) { - return OpenCvViewport.OptimizedRotation.NONE; - } - - if (streamRotation == OpenCvCameraRotation.SIDEWAYS_LEFT || streamRotation == OpenCvCameraRotation.SENSOR_NATIVE) { - return OpenCvViewport.OptimizedRotation.ROT_90_COUNTERCLOCWISE; - } else if (streamRotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) { - return OpenCvViewport.OptimizedRotation.ROT_90_CLOCKWISE; - } else if (streamRotation == OpenCvCameraRotation.UPSIDE_DOWN) { - return OpenCvViewport.OptimizedRotation.ROT_180; - } else { - return OpenCvViewport.OptimizedRotation.NONE; - } - } - - protected void setupViewport() { - viewport.setFpsMeterEnabled(fpsMeterDesired); - viewport.setRenderHook(PipelineRenderHook.INSTANCE); - } - - protected abstract OpenCvCameraRotation getDefaultRotation(); - - protected abstract int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation); - - protected abstract boolean cameraOrientationIsTiedToDeviceOrientation(); - - protected abstract boolean isStreaming(); - - protected Size getFrameSizeAfterRotation(int width, int height, OpenCvCameraRotation rotation) - { - int screenRenderedWidth, screenRenderedHeight; - int openCvRotateCode = mapRotationEnumToOpenCvRotateCode(rotation); - - if(openCvRotateCode == Core.ROTATE_90_CLOCKWISE || openCvRotateCode == Core.ROTATE_90_COUNTERCLOCKWISE) - { - //noinspection SuspiciousNameCombination - screenRenderedWidth = height; - //noinspection SuspiciousNameCombination - screenRenderedHeight = width; - } - else - { - screenRenderedWidth = width; - screenRenderedHeight = height; - } - - return new Size(screenRenderedWidth, screenRenderedHeight); - } - -} +/* + * Copyright (c) 2019 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.MovingStatistics; +import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator; +import io.github.deltacv.vision.external.PipelineRenderHook; +import org.opencv.android.Utils; +import org.opencv.core.*; +import org.opencv.imgproc.Imgproc; + +public abstract class OpenCvCameraBase implements OpenCvCamera { + + private OpenCvPipeline pipeline = null; + + private OpenCvViewport viewport; + private OpenCvCameraRotation rotation; + + private final Object pipelineChangeLock = new Object(); + + private Mat rotatedMat = new Mat(); + private Mat matToUseIfPipelineReturnedCropped; + private Mat croppedColorCvtedMat = new Mat(); + + private boolean isStreaming = false; + private boolean viewportEnabled = true; + + private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE; + ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; + boolean fpsMeterDesired = true; + + private Scalar brown = new Scalar(82, 61, 46, 255); + + private int frameCount = 0; + + private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator(); + + private double width; + private double height; + + public OpenCvCameraBase(OpenCvViewport viewport, boolean viewportEnabled) { + this.viewport = viewport; + this.rotation = getDefaultRotation(); + + this.viewportEnabled = viewportEnabled; + } + + @Override + public void showFpsMeterOnViewport(boolean show) { + viewport.setFpsMeterEnabled(show); + } + + @Override + public void pauseViewport() { + viewport.pause(); + } + + @Override + public void resumeViewport() { + viewport.resume(); + } + + @Override + public void setViewportRenderingPolicy(ViewportRenderingPolicy policy) { + viewport.setRenderingPolicy(policy); + } + + @Override + public void setViewportRenderer(ViewportRenderer renderer) { + this.desiredViewportRenderer = renderer; + } + + @Override + public void setPipeline(OpenCvPipeline pipeline) { + this.pipeline = pipeline; + } + + @Override + public int getFrameCount() { + return frameCount; + } + + @Override + public float getFps() { + return statistics.getAvgFps(); + } + + @Override + public int getPipelineTimeMs() { + return statistics.getAvgPipelineTime(); + } + + @Override + public int getOverheadTimeMs() { + return statistics.getAvgOverheadTime(); + } + + @Override + public int getTotalFrameTimeMs() { + return getTotalFrameTimeMs(); + } + + @Override + public int getCurrentPipelineMaxFps() { + return 0; + } + + @Override + public void startRecordingPipeline(PipelineRecordingParameters parameters) { + } + + @Override + public void stopRecordingPipeline() { + } + + protected void notifyStartOfFrameProcessing() { + statistics.newInputFrameStart(); + } + + public synchronized final void prepareForOpenCameraDevice() + { + if (viewportEnabled) + { + setupViewport(); + viewport.setRenderingPolicy(desiredRenderingPolicy); + } + } + + public synchronized final void prepareForStartStreaming(int width, int height, OpenCvCameraRotation rotation) + { + this.rotation = rotation; + this.statistics = new PipelineStatisticsCalculator(); + this.statistics.init(); + + Size sizeAfterRotation = getFrameSizeAfterRotation(width, height, rotation); + + this.width = sizeAfterRotation.width; + this.height = sizeAfterRotation.height; + + if(viewport != null) + { + // viewport.setSize(width, height); + viewport.setOptimizedViewRotation(getOptimizedViewportRotation(rotation)); + viewport.activate(); + } + } + + public synchronized final void cleanupForEndStreaming() { + matToUseIfPipelineReturnedCropped = null; + + if (viewport != null) { + viewport.deactivate(); + } + } + + protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { + statistics.newPipelineFrameStart(); + + Mat userProcessedFrame = null; + + int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation); + + if (rotateCode != -1) { + /* + * Rotate onto another Mat rather than doing so in-place. + * + * This does two things: + * 1) It seems that rotating by 90 or 270 in-place + * causes the backing buffer to be re-allocated + * since the width/height becomes swapped. This + * causes a problem for user code which makes a + * submat from the input Mat, because after the + * parent Mat is re-allocated the submat is no + * longer tied to it. Thus, by rotating onto + * another Mat (which is never re-allocated) we + * remove that issue. + * + * 2) Since the backing buffer does need need to be + * re-allocated for each frame, we reduce overhead + * time by about 1ms. + */ + Core.rotate(frame, rotatedMat, rotateCode); + frame = rotatedMat; + } + + final OpenCvPipeline pipelineSafe; + + // Grab a safe reference to what the pipeline currently is, + // since the user is allowed to change it at any time + synchronized (pipelineChangeLock) { + pipelineSafe = pipeline; + } + + if (pipelineSafe != null) { + if (pipelineSafe instanceof TimestampedOpenCvPipeline) { + ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp); + } + + statistics.beforeProcessFrame(); + + userProcessedFrame = pipelineSafe.processFrameInternal(frame); + + statistics.afterProcessFrame(); + } + + // Will point to whatever mat we end up deciding to send to the screen + final Mat matForDisplay; + + if (pipelineSafe == null) { + matForDisplay = frame; + } else if (userProcessedFrame == null) { + throw new OpenCvCameraException("User pipeline returned null"); + } else if (userProcessedFrame.empty()) { + throw new OpenCvCameraException("User pipeline returned empty mat"); + } else if (userProcessedFrame.cols() != frame.cols() || userProcessedFrame.rows() != frame.rows()) { + /* + * The user didn't return the same size image from their pipeline as we gave them, + * ugh. This makes our lives interesting because we can't just send an arbitrary + * frame size to the viewport. It re-uses framebuffers that are of a fixed resolution. + * So, we copy the user's Mat onto a Mat of the correct size, and then send that other + * Mat to the viewport. + */ + + if (userProcessedFrame.cols() > frame.cols() || userProcessedFrame.rows() > frame.rows()) { + /* + * What on earth was this user thinking?! They returned a Mat that's BIGGER in + * a dimension than the one we gave them! + */ + + throw new OpenCvCameraException("User pipeline returned frame of unexpected size"); + } + + //We re-use this buffer, only create if needed + if (matToUseIfPipelineReturnedCropped == null) { + matToUseIfPipelineReturnedCropped = frame.clone(); + } + + //Set to brown to indicate to the user the areas which they cropped off + matToUseIfPipelineReturnedCropped.setTo(brown); + + int usrFrmTyp = userProcessedFrame.type(); + + if (usrFrmTyp == CvType.CV_8UC1) { + /* + * Handle 8UC1 returns (masks and single channels of images); + * + * We have to color convert onto a different mat (rather than + * doing so in place) to avoid breaking any of the user's submats + */ + Imgproc.cvtColor(userProcessedFrame, croppedColorCvtedMat, Imgproc.COLOR_GRAY2RGBA); + userProcessedFrame = croppedColorCvtedMat; //Doesn't affect user's handle, only ours + } else if (usrFrmTyp != CvType.CV_8UC4 && usrFrmTyp != CvType.CV_8UC3) { + /* + * Oof, we don't know how to handle the type they gave us + */ + throw new OpenCvCameraException("User pipeline returned a frame of an illegal type. Valid types are CV_8UC1, CV_8UC3, and CV_8UC4"); + } + + //Copy the user's frame onto a Mat of the correct size + userProcessedFrame.copyTo(matToUseIfPipelineReturnedCropped.submat( + new Rect(0, 0, userProcessedFrame.cols(), userProcessedFrame.rows()))); + + //Send that correct size Mat to the viewport + matForDisplay = matToUseIfPipelineReturnedCropped; + } else { + /* + * Yay, smart user! They gave us the frame size we were expecting! + * Go ahead and send it right on over to the viewport. + */ + matForDisplay = userProcessedFrame; + } + + if (viewport != null) { + viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null)); + } + + statistics.endFrame(); + + if (viewport != null) { + viewport.notifyStatistics(statistics.getAvgFps(), statistics.getAvgPipelineTime(), statistics.getAvgOverheadTime()); + } + + frameCount++; + } + + + protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCameraRotation streamRotation) { + if (!cameraOrientationIsTiedToDeviceOrientation()) { + return OpenCvViewport.OptimizedRotation.NONE; + } + + if (streamRotation == OpenCvCameraRotation.SIDEWAYS_LEFT || streamRotation == OpenCvCameraRotation.SENSOR_NATIVE) { + return OpenCvViewport.OptimizedRotation.ROT_90_COUNTERCLOCWISE; + } else if (streamRotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) { + return OpenCvViewport.OptimizedRotation.ROT_90_CLOCKWISE; + } else if (streamRotation == OpenCvCameraRotation.UPSIDE_DOWN) { + return OpenCvViewport.OptimizedRotation.ROT_180; + } else { + return OpenCvViewport.OptimizedRotation.NONE; + } + } + + protected void setupViewport() { + viewport.setFpsMeterEnabled(fpsMeterDesired); + viewport.setRenderHook(PipelineRenderHook.INSTANCE); + } + + protected abstract OpenCvCameraRotation getDefaultRotation(); + + protected abstract int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation); + + protected abstract boolean cameraOrientationIsTiedToDeviceOrientation(); + + protected abstract boolean isStreaming(); + + protected Size getFrameSizeAfterRotation(int width, int height, OpenCvCameraRotation rotation) + { + int screenRenderedWidth, screenRenderedHeight; + int openCvRotateCode = mapRotationEnumToOpenCvRotateCode(rotation); + + if(openCvRotateCode == Core.ROTATE_90_CLOCKWISE || openCvRotateCode == Core.ROTATE_90_COUNTERCLOCKWISE) + { + //noinspection SuspiciousNameCombination + screenRenderedWidth = height; + //noinspection SuspiciousNameCombination + screenRenderedHeight = width; + } + else + { + screenRenderedWidth = width; + screenRenderedHeight = height; + } + + return new Size(screenRenderedWidth, screenRenderedHeight); + } + +} diff --git a/build.gradle b/build.gradle index d941433b..23b53910 100644 --- a/build.gradle +++ b/build.gradle @@ -9,14 +9,19 @@ buildscript { slf4j_version = "1.7.32" log4j_version = "2.17.1" opencv_version = "4.7.0-0" - apriltag_plugin_version = "main-SNAPSHOT" + apriltag_plugin_version = "2.1.0-B" skiko_version = "0.8.15" classgraph_version = "4.8.112" opencsv_version = "5.5.2" - env = findProperty('env') == 'release' ? 'release' : 'dev' + Penv = findProperty('env') + if(Penv != null && (Penv != 'dev' && Penv != 'release')) { + throw new GradleException("Invalid env property, must be 'dev' or 'release'") + } + + env = Penv == 'release' ? 'release' : 'dev' println("Current build is: $env") }