From 57942a46c056300aef66c5c28972b1cd4e7fefcc Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Tue, 29 Oct 2024 01:03:27 -0600 Subject: [PATCH] Improve restart routine & add more options to PluginOutput --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 40 +++++-- .../eocvsim/gui/dialog/PluginOutput.kt | 103 ++++++++++++++++-- .../gui/dialog/component/OutputPanel.kt | 13 ++- .../eocvsim/util/JavaProcess.java | 3 + .../plugin/loader/PluginClassLoader.kt | 21 +++- .../eocvsim/plugin/loader/PluginLoader.kt | 2 + .../eocvsim/plugin/loader/PluginManager.kt | 20 +++- .../eocvsim/plugin/security/Authority.kt | 21 ++-- 8 files changed, 181 insertions(+), 42 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 86fe763..2ba1961 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -58,9 +58,11 @@ import org.opencv.core.Size import org.openftc.easyopencv.TimestampedPipelineHandler import java.awt.Dimension import java.io.File +import java.lang.Thread.sleep import javax.swing.SwingUtilities import javax.swing.filechooser.FileFilter import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.jvm.Throws import kotlin.system.exitProcess /** @@ -361,7 +363,30 @@ class EOCVSim(val params: Parameters = Parameters()) { pluginManager.enablePlugins() - start() + try { + start() + } catch (e: InterruptedException) { + logger.warn("Main thread interrupted ($hexCode)", e) + } + + if(!destroying) { + destroy(DestroyReason.THREAD_EXIT) + } + + if (isRestarting) { + Thread.interrupted() //clear interrupted flag + EOCVSimFolder.lock?.lock?.close() + + JavaProcess.killSubprocessesOnExit = false + Thread { + JavaProcess.exec(Main::class.java, null, null) + }.start() + + sleep(1000) + } + + logger.info("-- End of EasyOpenCV Simulator v$VERSION ($hexCode) --") + exitProcess(0) } /** @@ -375,6 +400,7 @@ class EOCVSim(val params: Parameters = Parameters()) { * are explicitly updated within this method * @see init */ + @Throws(InterruptedException::class) private fun start() { if(Thread.currentThread() != eocvSimThread) { throw IllegalStateException("start() must be called from the EOCVSim thread") @@ -397,7 +423,7 @@ class EOCVSim(val params: Parameters = Parameters()) { } else null ) - //limit FPG + //limit FPS fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble() try { fpsLimiter.sync() @@ -407,16 +433,6 @@ class EOCVSim(val params: Parameters = Parameters()) { } logger.warn("Main thread interrupted ($hexCode)") - - if(!destroying) { - destroy(DestroyReason.THREAD_EXIT) - } - - if (isRestarting) { - Thread.interrupted() //clear interrupted flag - EOCVSimFolder.lock?.lock?.close() - JavaProcess.exec(Main::class.java, null, null) - } } /** diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt index c0c194c..8178073 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt @@ -28,6 +28,7 @@ import com.github.serivesmejia.eocvsim.gui.dialog.component.BottomButtonsPanel import com.github.serivesmejia.eocvsim.gui.dialog.component.OutputPanel import io.github.deltacv.eocvsim.plugin.loader.PluginManager import io.github.deltacv.eocvsim.plugin.loader.PluginSource +import io.github.deltacv.eocvsim.plugin.repository.PluginRepositoryManager import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -36,10 +37,11 @@ import kotlinx.coroutines.swing.Swing import java.awt.Dimension import java.awt.GridBagConstraints import java.awt.GridBagLayout -import java.awt.GridLayout import java.awt.Toolkit +import java.awt.Color import java.awt.datatransfer.StringSelection import javax.swing.* +import java.awt.Desktop import javax.swing.event.ChangeEvent import javax.swing.event.ChangeListener @@ -135,7 +137,7 @@ class PluginOutput( registerListeners() output.pack() - output.setSize(500, 350) + output.setSize(500, 365) appendDelegate.subscribe(this) @@ -182,6 +184,7 @@ class PluginOutput( ) if(dialogResult == JOptionPane.YES_OPTION) { + output.isVisible = false eocvSim.restart() } @@ -190,8 +193,8 @@ class PluginOutput( } private fun makePluginManagerPanel(): JPanel { - val panel = JPanel() - panel.layout = GridBagLayout() + val pluginsPanel = JPanel() + pluginsPanel.layout = GridBagLayout() if(pluginManager.loaders.isEmpty()) { // center vertically and horizontally @@ -209,7 +212,7 @@ class PluginOutput( } // Add the label to the panel with the constraints - panel.add(noPluginsLabel, constraints) + pluginsPanel.add(noPluginsLabel, constraints) } else { val tabbedPane = JTabbedPane(JTabbedPane.LEFT) @@ -291,7 +294,6 @@ class PluginOutput( fun refreshButtons() { disableButton.isEnabled = loader.shouldEnable enableButton.isEnabled = !loader.shouldEnable - } refreshButtons() @@ -336,7 +338,7 @@ class PluginOutput( tabbedPane.addTab(loader.pluginName, pluginPanel) } - panel.add(tabbedPane, GridBagConstraints().apply { + pluginsPanel.add(tabbedPane, GridBagConstraints().apply { gridx = 0 gridy = 0 weightx = 1.0 @@ -345,6 +347,93 @@ class PluginOutput( }) } + val panel = JPanel() + + panel.layout = GridBagLayout() + panel.add(pluginsPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 0 + weightx = 1.0 + weighty = 1.0 + fill = GridBagConstraints.BOTH + }) + + val bottomButtonsPanel = JPanel() + bottomButtonsPanel.layout = BoxLayout(bottomButtonsPanel, BoxLayout.PAGE_AXIS) + + val openPluginsFolderButton = JButton("Open plugins folder") + + openPluginsFolderButton.addActionListener { + val pluginsFolder = PluginManager.PLUGIN_FOLDER + + if(pluginsFolder.exists() && Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(pluginsFolder) + } else { + JOptionPane.showMessageDialog( + output, + "Unable to open plugins folder, the folder does not exist or the operation is unsupported.", + "Operation failed", + JOptionPane.ERROR_MESSAGE + ) + } + } + + val startFreshButton = JButton("Start fresh") + + startFreshButton.addActionListener { + val dialogResult = JOptionPane.showConfirmDialog( + output, + "Are you sure you want to start fresh? This will remove all plugins from all sources.", + "Start fresh", + JOptionPane.YES_NO_OPTION + ) + + if(dialogResult == JOptionPane.YES_OPTION) { + eocvSim?.config?.flags?.set("startFresh", true) + + PluginRepositoryManager.REPOSITORY_FILE.delete() + PluginRepositoryManager.CACHE_FILE.delete() + + shouldAskForRestart = true + checkShouldAskForRestart() + } + } + + val closeButton = JButton("Close") + + closeButton.addActionListener { + close() + } + + val buttonsPanel = JPanel() + buttonsPanel.layout = BoxLayout(buttonsPanel, BoxLayout.LINE_AXIS) + buttonsPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) + + buttonsPanel.add(Box.createHorizontalGlue()) + + buttonsPanel.add(openPluginsFolderButton) + buttonsPanel.add(Box.createRigidArea(Dimension(5, 0))) + buttonsPanel.add(startFreshButton) + buttonsPanel.add(Box.createRigidArea(Dimension(5, 0))) + buttonsPanel.add(closeButton) + + buttonsPanel.add(Box.createHorizontalGlue()) + + bottomButtonsPanel.add(buttonsPanel) + + // Set a thin light gray line border with padding + bottomButtonsPanel.border = BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(10, 10, 10, 10), // Padding inside the border + JScrollPane().border + ) + + panel.add(bottomButtonsPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 2 + weightx = 1.0 + fill = GridBagConstraints.HORIZONTAL + }) + return panel } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt index f28e6f4..b80fe5c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt @@ -23,6 +23,7 @@ package com.github.serivesmejia.eocvsim.gui.dialog.component +import com.formdev.flatlaf.FlatLaf import java.awt.Dimension import java.awt.GridBagConstraints import java.awt.GridBagLayout @@ -31,6 +32,7 @@ import java.awt.datatransfer.StringSelection import java.awt.Font import java.io.InputStream import javax.swing.* +import kotlin.math.roundToInt class OutputPanel( @@ -60,7 +62,16 @@ class OutputPanel( outputArea.highlighter = null // set the background color to a darker tone - outputArea.background = outputArea.background.darker() + outputArea.background = if(FlatLaf.isLafDark()) { + outputArea.background.darker() + } else { + java.awt.Color( + (outputArea.background.red * 0.95).roundToInt(), + (outputArea.background.green * 0.95).roundToInt(), + (outputArea.background.blue * 0.95).roundToInt(), + 255 + ) + } val outputScroll = JScrollPane(outputArea) outputScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java index a7c7806..4b4451a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java @@ -80,6 +80,8 @@ private JavaProcess() {} private static int count; + public static boolean killSubprocessesOnExit = true; + private static final Logger logger = LoggerFactory.getLogger(JavaProcess.class); /** @@ -143,6 +145,7 @@ public static int execClasspath(Class klass, ProcessIOReceiver ioReceiver, Strin } private static void killOnExit(Process process) { + if(!killSubprocessesOnExit) return; Runtime.getRuntime().addShutdownHook(new Thread(process::destroy)); } 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 3060a3f..bc557d6 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 @@ -31,6 +31,7 @@ import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingMethodBlackl import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageBlacklist import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageWhitelist import java.io.ByteArrayOutputStream +import java.lang.ref.WeakReference import java.io.File import java.io.IOException import java.io.InputStream @@ -50,6 +51,8 @@ class PluginClassLoader( val pluginContextProvider: () -> PluginContext ) : ClassLoader() { + private var additionalZipFiles = mutableListOf>() + private val zipFile = try { ZipFile(pluginJar) } catch (e: Exception) { @@ -172,11 +175,8 @@ class PluginClassLoader( 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) { - } + // Construct a URL for the resource inside the plugin JAR + return URL("jar:file:${pluginJar.absolutePath}!/$name") } else { resourceFromClasspath(name)?.let { return it } } @@ -198,9 +198,13 @@ class PluginClassLoader( if (entry != null) { try { + additionalZipFiles.add(WeakReference(zipFile)) return zipFile.getInputStream(entry) } catch (e: Exception) { + zipFile.close() } + } else { + zipFile.close() } } @@ -256,6 +260,13 @@ class PluginClassLoader( return null } + fun close() { + zipFile.close() + for(ref in additionalZipFiles) { + ref.get()?.close() + } + } + 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/PluginLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt index dcb36cc..2ae9631 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 @@ -280,6 +280,8 @@ class PluginLoader( fileSystem.close() enabled = false EventHandler.banClassLoader(pluginClassLoader) + + pluginClassLoader.close() } /** 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 526ed1c..3e9fb97 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 @@ -119,11 +119,23 @@ class PluginManager(val eocvSim: EOCVSim) { repositoryManager.init() - val pluginFiles = mutableListOf() - pluginFiles.addAll(repositoryManager.resolveAll()) + val pluginFilesInFolder = PLUGIN_FOLDER.listFiles()?.let { + it.filter { file -> file.extension == "jar" } + } ?: emptyList() - PLUGIN_FOLDER.listFiles()?.let { - pluginFiles.addAll(it.filter { it.extension == "jar" }) + _pluginFiles.addAll(repositoryManager.resolveAll()) + + if(eocvSim.config.flags.getOrDefault("startFresh", false)) { + logger.warn("startFresh = true, deleting all plugins in the plugins folder") + + for (file in pluginFilesInFolder) { + file.delete() + } + + eocvSim.config.flags["startFresh"] = false + eocvSim.configManager.saveToFile() + } else { + _pluginFiles.addAll(pluginFilesInFolder) } for (file in pluginFiles) { diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt index edc1102..20d7129 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt @@ -55,6 +55,8 @@ object AuthorityFetcher { const val AUTHORITY_SERVER_URL = "https://raw.githubusercontent.com/deltacv/Authorities/refs/heads/master" + fun sectionRegex(name: String) = Regex("\\[\\Q$name\\E](?:\\n[^\\[]+)*") + private val AUTHORITIES_FILE = File(EOCVSimFolder, "authorities.toml") private val TTL_DURATION_MS = TimeUnit.HOURS.toMillis(8) @@ -92,6 +94,7 @@ object AuthorityFetcher { } } catch (e: Exception) { logger.error("Failed to read authorities file", e) + AUTHORITIES_FILE.delete() } } @@ -123,20 +126,12 @@ object AuthorityFetcher { // Load existing authorities if the file exists if (AUTHORITIES_FILE.exists()) { val existingToml = AUTHORITIES_FILE.readText() - if(existingToml.contains("[$name]")) { - // remove the existing authority. we need to delete the [$name] and the next two lines - val lines = existingToml.lines().toMutableList() - val index = lines.indexOfFirst { it == "[$name]" } - - if(index != -1) { - lines.removeAt(index) - lines.removeAt(index) - lines.removeAt(index) - } - sb.append(lines.joinToString("\n")) - } - sb.append(existingToml) + sb.append(if(existingToml.contains("[$name]")) { + existingToml.replace(sectionRegex(name), "") + } else { + existingToml + }) } // Append new authority information