Skip to content

Comments

Adjust Install workflow of MicrobotPluginManager#1423

Merged
gmason0 merged 1 commit intochsami:developmentfrom
gmason0:development
Aug 23, 2025
Merged

Adjust Install workflow of MicrobotPluginManager#1423
gmason0 merged 1 commit intochsami:developmentfrom
gmason0:development

Conversation

@gmason0
Copy link
Contributor

@gmason0 gmason0 commented Aug 23, 2025

This pull request aims to remedy an issue with the MicrobotPluginManager.

Previously this class would attempt to reload all plugins when installing a plugin from the plugin hub. I have changed this by adjusting the work flow of the hub panel to target an individual internalName instead of reloading all classes.

I have also improved how the manifest is stored & referenced in the plugin hub. This maintains a hash map of the internal plugin name -> manifest, this is refreshed based on an interval to ensure its up to date.

Summary by CodeRabbit

  • New Features

    • Manifest-driven external plugin catalog with periodic automatic refresh and local caching.
    • One-click install with client-version checks, integrity (SHA-256) verification, per-plugin sideload loading, and automatic sync of installed vs. loaded plugins.
  • Bug Fixes

    • Prevents duplicate plugin loads, improves dependency resolution (cycle detection/topological ordering), and auto-heals corrupt local plugin lists.
    • More robust error handling and logging for installs and manifest parsing.
  • Refactor

    • Plugin Hub now uses manager-provided manifests, shows usage counts, adds retry UI on fetch failures, and scales icons for consistent thumbnails.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

Walkthrough

Adds a manifest-backed cache with scheduled refreshes in MicrobotPluginManager, enables single-plugin sideload install/load and sync, rewrites dependency-aware plugin loading, and changes the UI to read manifests from the manager, fetch plugin counts separately, and scale plugin icons.

Changes

Cohort / File(s) Summary
External plugin manager overhaul
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java
Adds manifestMap with getManifestMap(), loadManifest() with scheduled refresh, rewritten install() to download+verify jars, new loadSideLoadPlugin(String) and syncPlugins(), revised loadSideLoadPlugins(), improved loadPlugins(...) with duplicate detection, dependency graph build, cycle detection and topological sort, and JsonSyntaxException/error handling.
Plugin hub panel reload and UI updates
runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java
Removes lastManifest; reload now sources manifests via microbotPluginManager.getManifestMap() and plugin counts via microbotPluginClient.getPluginCounts(); adds reloadPluginList(Collection, Map) overload, updates onExternalPluginsChanged, shows retry UI on fetch failure, and scales icons to 48×70 with high-quality rendering.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Panel as MicrobotPluginHubPanel
  participant Manager as MicrobotPluginManager
  participant Client as MicrobotPluginClient

  User->>Panel: Open / Trigger reload
  Panel->>Manager: getManifestMap()
  Panel->>Client: getPluginCounts()
  alt success
    Panel->>Panel: reloadPluginList(manifests, counts)
    Panel-->>User: Render list
  else failure
    Panel-->>User: Show retry UI
  end

  note over Manager: Background scheduled manifest refresh
  Manager->>Manager: loadManifest()
  Manager-->>Panel: ExternalPluginsChanged
  Panel->>Manager: getManifestMap()
  Panel->>Client: getPluginCounts()
  Panel->>Panel: reloadPluginList(...)
Loading
sequenceDiagram
  autonumber
  actor User
  participant Panel
  participant Manager
  participant Repo as PluginRepo
  participant FS as Filesystem
  participant CL as MicrobotPluginClassLoader
  participant Bus as EventBus

  User->>Panel: Install plugin
  Panel->>Manager: install(manifest)
  Manager->>Repo: Download jar
  Manager->>Manager: Verify SHA-256
  Manager->>FS: Write jar + update installed list
  Manager->>Manager: Update manifestMap
  Manager->>Manager: loadSideLoadPlugin(internalName)
  Manager->>FS: Validate jar + hash
  Manager->>CL: Load classes
  Manager->>Manager: loadPlugins(... dependency resolution ...)
  Manager->>Bus: Post ExternalPluginsChanged
  Bus-->>Panel: ExternalPluginsChanged
Loading
sequenceDiagram
  autonumber
  participant Manager
  participant FS as Filesystem
  participant Runtime as LoadedPlugins
  participant Bus as EventBus

  Manager->>Manager: syncPlugins()
  Manager->>FS: Read installed list
  Manager->>Runtime: Identify loaded but not installed
  Manager->>Runtime: Unload extras
  Manager->>FS: Enumerate sideload jars
  loop jars
    alt installed & not loaded
      Manager->>Manager: loadSideLoadPlugin(name)
    else skip
    end
  end
  Manager->>Bus: Post ExternalPluginsChanged (if changed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

coderabbitai bot added a commit that referenced this pull request Aug 23, 2025
Docstrings generation was requested by @g-mason0.

* #1423 (comment)

The following files were modified:

* `runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java`
* `runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java`
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

Note

Generated docstrings for this pull request at #1424

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (1)

574-610: Consider handling edge cases in the reloadPluginList workflow

The current implementation has potential issues:

  1. If microbotPluginClient.getPluginCounts() throws an exception, the error UI is shown but reloadPluginList(manifestCollection, pluginCounts) is never called, leaving the plugin list empty.
  2. The refreshing flag is set to visible early but may not be properly reset if exceptions occur.

Consider restructuring the error handling to ensure the plugin list is still loaded even if plugin counts fail:

 executor.submit(() ->
 {
 	Collection<MicrobotPluginManifest> manifestCollection = microbotPluginManager.getManifestMap().values();
 
 	Map<String, Integer> pluginCounts = Collections.emptyMap();
 	try
 	{
 		pluginCounts = microbotPluginClient.getPluginCounts();
 	}
 	catch (IOException e)
 	{
 		log.warn("Unable to download plugin counts", e);
-		SwingUtilities.invokeLater(() ->
-		{
-			refreshing.setVisible(false);
-			mainPanel.add(new JLabel("Downloading the plugin manifest failed"));
-
-			JButton retry = new JButton("Retry");
-			retry.addActionListener(l -> reloadPluginList());
-			mainPanel.add(retry);
-			mainPanel.revalidate();
-		});
 	}
 
 	reloadPluginList(manifestCollection, pluginCounts);
 });
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (3)

84-86: Consider thread-safety implications of the manifestMap getter

The manifestMap field is exposed via a public getter, allowing external code to modify the underlying ConcurrentHashMap. This could lead to unexpected behavior if external code modifies the map directly.

Return an unmodifiable view to prevent external modifications:

 @Getter
-private final Map<String, MicrobotPluginManifest> manifestMap = new ConcurrentHashMap<>();
+private final Map<String, MicrobotPluginManifest> manifestMap = new ConcurrentHashMap<>();
+
+public Map<String, MicrobotPluginManifest> getManifestMap() {
+    return Collections.unmodifiableMap(manifestMap);
+}

87-99: Consider adding retry logic for manifest loading

The loadManifest() method silently catches all exceptions and only logs them. This could leave the manifestMap empty if the initial load fails, potentially causing issues for dependent components.

Consider adding a retry mechanism or at least maintaining the previous manifest state on failure:

 private void loadManifest() {
     try {
         List<MicrobotPluginManifest> manifests = microbotPluginClient.downloadManifest();
+        Map<String, MicrobotPluginManifest> newManifestMap = new ConcurrentHashMap<>();
-        manifestMap.clear();
         for (MicrobotPluginManifest manifest : manifests) {
-            manifestMap.put(manifest.getInternalName(), manifest);
+            newManifestMap.put(manifest.getInternalName(), manifest);
         }
+        manifestMap.clear();
+        manifestMap.putAll(newManifestMap);
         log.info("Loaded {} plugin manifests.", manifestMap.size());
         eventBus.post(new ExternalPluginsChanged());
     } catch (Exception e) {
         log.error("Failed to fetch plugin manifests", e);
     }
 }

535-633: Duplicate detection logic could be more robust

The current duplicate detection only checks if the exact class is already loaded (Line 541-551). However, it doesn't check for the same plugin loaded from different classloaders or with different versions.

Consider checking by plugin name/descriptor instead of just class reference:

-Set<Class<?>> alreadyLoaded = pluginManager.getPlugins().stream()
-    .map(Object::getClass)
-    .collect(Collectors.toSet());
+Set<String> alreadyLoadedNames = pluginManager.getPlugins().stream()
+    .filter(p -> p.getClass().isAnnotationPresent(PluginDescriptor.class))
+    .map(p -> p.getClass().getAnnotation(PluginDescriptor.class).name())
+    .collect(Collectors.toSet());

 for (Class<?> clazz : plugins)
 {
-    if (alreadyLoaded.contains(clazz))
-    {
-        log.debug("Plugin {} is already loaded, skipping duplicate.", clazz.getSimpleName());
-        continue;
-    }
     PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class);
 
     if (pluginDescriptor == null)
     {
         if (clazz.getSuperclass() == Plugin.class)
         {
             log.error("Class {} is a plugin, but has no plugin descriptor", clazz);
         }
         continue;
     }
+    
+    if (alreadyLoadedNames.contains(pluginDescriptor.name()))
+    {
+        log.debug("Plugin {} is already loaded, skipping duplicate.", pluginDescriptor.name());
+        continue;
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7a184aa and 4392886.

📒 Files selected for processing (2)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (7 hunks)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (1)
runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java (1)
  • RuneLiteProperties (35-162)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (2)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (1)

612-672: LGTM! Well-structured plugin list reload logic

The new overloaded reloadPluginList method properly:

  • Filters out disabled plugins
  • Correctly indexes manifests and loaded plugins
  • Handles plugin installation state detection
  • Properly resets the refreshing state on the EDT
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (1)

466-497: LGTM! Improved plugin loading workflow

The refactored loadSideLoadPlugins method properly:

  • Synchronizes plugins before loading
  • Filters out non-JAR files and non-installed plugins
  • Avoids reloading already loaded plugins
  • Delegates to the single-plugin loader

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (2)

394-457: Good: single-plugin sideload path with hash validation and cleanup

  • Validates installation, avoids duplicate load, verifies hash, deletes corrupted JAR, updates plugins.json, and posts ExternalPluginsChanged on success. This aligns with prior feedback.

171-238: Harden install() against empty response bodies and invalid hashes; optionally surface failures to UI

  • response.body() can be null; bytes() would NPE. Guard and log.
  • verifyHash throws IllegalArgumentException on null/empty inputs; either validate manifest.getSha256() up front or catch the exception.
  • Optional: write to a temp file and perform an atomic move to avoid partial JARs on crashes.
  • Optional: emit a user-visible event on failure (matches a prior review suggestion).
                 try (Response response = okHttpClient.newCall(request).execute())
                 {
                     if (!response.isSuccessful())
                     {
                         log.error("Error downloading plugin: {}, code: {}", manifest.getInternalName(), response.code());
                         return;
                     }
 
-                    byte[] jarData = response.body().bytes();
+                    if (response.body() == null)
+                    {
+                        log.error("Empty response body for plugin: {}", manifest.getInternalName());
+                        return;
+                    }
+                    byte[] jarData = response.body().bytes();
 
                     // Verify the SHA-256 hash
-                    if (!verifyHash(jarData, manifest.getSha256()))
+                    if (manifest.getSha256() == null || manifest.getSha256().isEmpty())
+                    {
+                        log.error("Missing SHA-256 in manifest for: {}", manifest.getInternalName());
+                        return;
+                    }
+                    if (!verifyHash(jarData, manifest.getSha256()))
                     {
                         log.error("Plugin hash verification failed for: {}", manifest.getInternalName());
                         return;
                     }
 
                     manifestMap.put(manifest.getInternalName(), manifest);
                     // Save the jar file
                     File pluginFile = getPluginJarFile(manifest.getInternalName());
-                    Files.write(jarData, pluginFile);
+                    File tmp = File.createTempFile(manifest.getInternalName(), ".jar.tmp", PLUGIN_DIR);
+                    Files.write(jarData, tmp);
+                    Files.move(tmp, pluginFile);
@@
-            catch (IOException e)
+            catch (IOException | IllegalArgumentException e)
             {
                 log.error("Error installing plugin: {}", manifest.getInternalName(), e);
             }

Optionally, post a failure event alongside logs so the UI can inform the user (event class omitted here to avoid API assumptions).

🧹 Nitpick comments (6)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (6)

84-86: Expose manifests read-only to callers

Right now external code can mutate manifestMap via the Lombok-generated getter. Since you already refresh the map internally and other components should treat it as read-only state, return an unmodifiable view to prevent accidental writes from outside.

Apply this diff here (remove Lombok getter on the field):

-	@Getter
-    private final Map<String, MicrobotPluginManifest> manifestMap = new ConcurrentHashMap<>();
+    // Expose via a read-only accessor to prevent external mutation
+    private final Map<String, MicrobotPluginManifest> manifestMap = new ConcurrentHashMap<>();

Then add this accessor elsewhere in the class:

public Map<String, MicrobotPluginManifest> getManifestMap() {
    return Collections.unmodifiableMap(manifestMap);
}

87-99: Post ExternalPluginsChanged only when manifests actually change

loadManifest() currently posts ExternalPluginsChanged on every refresh, which can wake UI unnecessarily. Build a new map, compare (at least by keys + version/hash), and only swap + post on change. This also avoids any transient “empty” window during clear()/put().

-        try {
-            List<MicrobotPluginManifest> manifests = microbotPluginClient.downloadManifest();
-            manifestMap.clear();
-            for (MicrobotPluginManifest manifest : manifests) {
-                manifestMap.put(manifest.getInternalName(), manifest);
-            }
-            log.info("Loaded {} plugin manifests.", manifestMap.size());
-            eventBus.post(new ExternalPluginsChanged());
+        try {
+            List<MicrobotPluginManifest> manifests = microbotPluginClient.downloadManifest();
+            Map<String, MicrobotPluginManifest> next = new HashMap<>(manifests.size());
+            for (MicrobotPluginManifest m : manifests) {
+                next.put(m.getInternalName(), m);
+            }
+            boolean changed = !next.keySet().equals(manifestMap.keySet())
+                || next.entrySet().stream().anyMatch(e -> {
+                    MicrobotPluginManifest cur = manifestMap.get(e.getKey());
+                    return cur == null || !Objects.equals(cur.getSha256(), e.getValue().getSha256());
+                });
+            if (changed) {
+                manifestMap.clear();
+                manifestMap.putAll(next);
+                log.info("Loaded {} plugin manifests.", manifestMap.size());
+                eventBus.post(new ExternalPluginsChanged());
+            } else {
+                log.debug("Plugin manifests unchanged ({} entries), skipping event.", manifestMap.size());
+            }
         } catch (Exception e) {
             log.error("Failed to fetch plugin manifests", e);
         }

147-147: Auto-heal corrupt plugins.json on parse errors

You now catch JsonSyntaxException, but subsequent reads will continue to fail until the file is fixed. Consider auto-healing to an empty list when JSON is corrupt, and keep the existing log.

-        catch (IOException | JsonSyntaxException e)
-        {
-            log.error("Error reading Microbot plugin list", e);
-        }
+        catch (IOException | JsonSyntaxException e)
+        {
+            log.error("Error reading Microbot plugin list", e);
+            // Auto-heal corrupt file to reduce repeated failures
+            try {
+                Files.asCharSink(PLUGIN_LIST, StandardCharsets.UTF_8).write("[]");
+            } catch (IOException ioEx) {
+                log.warn("Failed to auto-heal plugins.json", ioEx);
+            }
+        }

424-434: Notify UI after quarantining a bad JAR

When a hash mismatch is detected you delete the file and update plugins.json, but the UI may not refresh immediately. Consider posting an event to inform the hub panel.

             if (!verifyHash(fileBytes, manifest.getSha256()))
             {
                 log.error("Hash mismatch for plugin {}. Skipping load.", internalName);
                 pluginFile.delete();
                 List<String> plugins = getInstalledPlugins();
                 plugins.remove(internalName);
                 saveInstalledPlugins(plugins);
+                eventBus.post(new ExternalPluginsChanged());
                 return;
             }

459-490: Reuse the same internalName mapping as syncPlugins()

loadSideLoadPlugins() also infers loadedInternalNames via PluginDescriptor.name(). Use the same helper to avoid drift and accidental duplicates/removals.

-        Set<String> loadedInternalNames = pluginManager.getPlugins().stream()
+        Set<String> loadedInternalNames = pluginManager.getPlugins().stream()
             .filter(p -> p.getClass().isAnnotationPresent(PluginDescriptor.class))
             .filter(p -> p.getClass().getAnnotation(PluginDescriptor.class).isExternal())
-            .map(p -> p.getClass().getAnnotation(PluginDescriptor.class).name())
+            .map(this::getPluginInternalName)
             .collect(Collectors.toSet());

528-626: Only increment progress on successful instantiation; prefer parameterized logging

Minor polish:

  • loaded++ increments even when instantiation fails, inflating progress.
  • Switch to parameterized logging instead of string concatenation.
-        int loaded = 0;
+        int loaded = 0;
         List<Plugin> newPlugins = new ArrayList<>();
         for (Class<? extends Plugin> pluginClazz : sortedPlugins)
         {
             Plugin plugin;
             try
             {
                 plugin = instantiate(pluginManager.getPlugins(), (Class<Plugin>) pluginClazz);
-                log.info("Microbot pluginManager loaded " + plugin.getClass().getSimpleName());
+                log.info("Microbot pluginManager loaded {}", plugin.getClass().getSimpleName());
                 newPlugins.add(plugin);
                 pluginManager.addPlugin(plugin);
+                loaded++;
             }
             catch (PluginInstantiationException ex)
             {
                 log.error("Error instantiating plugin!", ex);
             }
 
-            loaded++;
             if (onPluginLoaded != null)
             {
                 onPluginLoaded.accept(loaded, sortedPlugins.size());
             }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4392886 and f0a8704.

📒 Files selected for processing (2)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (7 hunks)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-23T16:17:37.571Z
Learnt from: g-mason0
PR: chsami/Microbot#1423
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java:366-389
Timestamp: 2025-08-23T16:17:37.571Z
Learning: The ScheduledExecutorService injected into MicrobotPluginManager (and other RuneLite classes) is configured as a single-threaded executor via Executors.newSingleThreadScheduledExecutor() at RuneLiteModule.java line 130, wrapped in ExecutorServiceExceptionLogger. This ensures all async tasks execute sequentially, preventing race conditions.

Applied to files:

  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java
🧬 Code graph analysis (1)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (1)
runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java (1)
  • RuneLiteProperties (35-162)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (3)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (3)

34-34: LGTM: imports align with new usage

Adding JsonSyntaxException and TimeUnit/@Getter imports matches later code paths. No issues.

Also applies to: 40-41


101-134: Initialization and scheduled refresh look sound

  • Ensuring PLUGIN_DIR and PLUGIN_LIST exist is good.
  • Immediate loadManifest() followed by a scheduled refresh every 10 minutes is appropriate.
  • Given DI provides a single-threaded ScheduledExecutorService, manifest refreshes are serialized.

366-389: Use configName() (or a normalized internal name) for the plugin key to match the manifest’s internalName

The current syncPlugins() collects external plugins by PluginDescriptor.name(), but the installed‐plugins list is based on the manifest’s internalName. Since name() is intended as a human‐readable display name (and may include spaces, mixed case, etc.) this can lead to false removals. Instead, centralize the “plugin key” logic into a helper that returns configName() (when present) or falls back to name(), normalized to the same form used in the manifest.

• Location: MicrobotPluginManager.syncPlugins()
– Change the collector’s key extractor
• Add helper: getPluginKey(Plugin)

Suggested diff:

--- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java
@@ public void syncPlugins()
-    Map<String, Plugin> loadedByInternalName = pluginManager.getPlugins().stream()
-        .filter(p -> p.getClass().isAnnotationPresent(PluginDescriptor.class))
-        .filter(p -> p.getClass().getAnnotation(PluginDescriptor.class).isExternal())
-        .collect(Collectors.toMap(
-            p -> p.getClass().getAnnotation(PluginDescriptor.class).name(),
-            Function.identity(),
-            (a, b) -> a
-        ));
+    Map<String, Plugin> loadedByInternalName = pluginManager.getPlugins().stream()
+        .filter(p -> p.getClass().isAnnotationPresent(PluginDescriptor.class))
+        .filter(p -> p.getClass().getAnnotation(PluginDescriptor.class).isExternal())
+        .collect(Collectors.toMap(
+            this::getPluginKey,
+            Function.identity(),
+            (a, b) -> a
+        ));

Add this helper to the class:

private String getPluginKey(Plugin p) {
    PluginDescriptor d = p.getClass().getAnnotation(PluginDescriptor.class);
    if (d == null) {
        return p.getClass().getSimpleName().toLowerCase(Locale.ROOT);
    }
    // Use configName() if explicitly set; otherwise fall back to name().
    String key = d.configName().isEmpty() ? d.name() : d.configName();
    return key.toLowerCase(Locale.ROOT);
}

configName is documented as the “internal name used in the config” and defaults to "" if unset
• Normalizing to lowercase ensures exact matches against manifest.getInternalName() (which you may also normalize)
• This avoids any mismatch if name() contains spaces or case differences

Please verify that all external plugins’ descriptors set configName to the same value as their manifest’s internalName so that this lookup aligns correctly.

…instead of trying to re-load all sideloaded plugins
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (1)

639-646: Loaded-plugin lookup should use PluginDescriptor.name(), not class simple name

Indexing loaded plugins by getSimpleName() can desync the UI from real installs/updates when class names diverge from internalName. Use @PluginDescriptor.name() to align with MicrobotPluginManifest.internalName.

Apply:

-        Map<String, Collection<Plugin>> pluginsByName = loadedPlugins.stream()
-                .collect(Collectors.groupingBy(
-                        p -> p.getClass().getSimpleName().toLowerCase(Locale.ROOT),
-                        LinkedHashMap::new,
-                        Collectors.toCollection(LinkedHashSet::new) // stable, no dups
-                ));
+        Map<String, Collection<Plugin>> pluginsByName = loadedPlugins.stream()
+                .collect(Collectors.groupingBy(
+                        p -> {
+                            PluginDescriptor d = p.getClass().getAnnotation(PluginDescriptor.class);
+                            String key = (d != null && d.name() != null) ? d.name() : p.getClass().getSimpleName();
+                            return key.toLowerCase(Locale.ROOT);
+                        },
+                        LinkedHashMap::new,
+                        Collectors.toCollection(LinkedHashSet::new) // stable, no dups
+                ));
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (1)

197-201: Path traversal risk when constructing plugin jar path

getPluginJarFile concatenates internalName + ".jar" directly. A malicious or malformed internalName (e.g., "../../foo") can escape PLUGIN_DIR.

Harden path building:

 private File getPluginJarFile(String internalName)
 {
-    return new File(PLUGIN_DIR, internalName + ".jar");
+    // sanitize the filename and enforce directory containment
+    String safe = internalName.replaceAll("[^A-Za-z0-9._-]", "_");
+    File f = new File(PLUGIN_DIR, safe + ".jar");
+    try
+    {
+        String base = PLUGIN_DIR.getCanonicalPath() + File.separator;
+        String target = f.getCanonicalPath();
+        if (!target.startsWith(base))
+        {
+            throw new IllegalArgumentException("Refusing to write outside plugin directory for internalName=" + internalName);
+        }
+    }
+    catch (IOException ioe)
+    {
+        throw new RuntimeException("Failed to resolve plugin jar path", ioe);
+    }
+    return f;
 }
♻️ Duplicate comments (2)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (1)

723-726: Simplified ExternalPluginsChanged handler — LGTM

Hooking the event to the no-arg reloadPluginList() removes duplication and keeps all error/logging paths centralized.

runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (1)

202-269: Installation flow lacks robust failure signaling and misses some exception types

  • Any IllegalArgumentException from verifyHash (e.g., missing/empty expected hash) escapes the IOException catch and gets logged only by the executor wrapper, leaving the UI stuck in “Installing”.

  • Early-return failure paths (disabled, version-incompatible, invalid URL, HTTP error, hash mismatch) don’t notify the UI; the panel will not reset unless a separate event occurs.

  • Catch RuntimeException (or at least IllegalArgumentException) alongside IOException.

  • Post an ExternalPluginsChanged (or a dedicated PluginInstallationFailed) on all failure exits so the UI can refresh.

Apply:

 public void install(MicrobotPluginManifest manifest)
 {
     executor.execute(() -> {
         // Check if plugin is disabled
         if (manifest.isDisable())
         {
             log.error("Plugin {} is disabled and cannot be installed.", manifest.getInternalName());
+            eventBus.post(new ExternalPluginsChanged());
             return;
         }

         // Check version compatibility before installing
         if (!isClientVersionCompatible(manifest.getMinClientVersion()))
         {
             log.error("Plugin {} requires client version {} or higher, but current version is {}. Installation aborted.",
                 manifest.getInternalName(), manifest.getMinClientVersion(), RuneLiteProperties.getMicrobotVersion());
+            eventBus.post(new ExternalPluginsChanged());
             return;
         }

         try
         {
             HttpUrl url = microbotPluginClient.getJarURL(manifest);
             if (url == null)
             {
 
                 log.error("Invalid URL for plugin: {}", manifest.getInternalName());
+                eventBus.post(new ExternalPluginsChanged());
                 return;
             }
 
             Request request = new Request.Builder()
                 .url(url)
                 .build();
 
             try (Response response = okHttpClient.newCall(request).execute())
             {
                 if (!response.isSuccessful())
                 {
                     log.error("Error downloading plugin: {}, code: {}", manifest.getInternalName(), response.code());
+                    eventBus.post(new ExternalPluginsChanged());
                     return;
                 }
 
                 byte[] jarData = response.body().bytes();
 
                 // Verify the SHA-256 hash
                 if (!verifyHash(jarData, manifest.getSha256()))
                 {
                     log.error("Plugin hash verification failed for: {}", manifest.getInternalName());
+                    eventBus.post(new ExternalPluginsChanged());
                     return;
                 }
 
                 manifestMap.put(manifest.getInternalName(), manifest);
                 // Save the jar file
                 File pluginFile = getPluginJarFile(manifest.getInternalName());
                 Files.write(jarData, pluginFile);
                 List<String> plugins = getInstalledPlugins();
                 if (!plugins.contains(manifest.getInternalName()))
                 {
                     plugins.add(manifest.getInternalName());
                     saveInstalledPlugins(plugins);
                 }
                 loadSideLoadPlugin(manifest.getInternalName());
             }
         }
-        catch (IOException e)
+        catch (IOException | RuntimeException e)
         {
             log.error("Error installing plugin: {}", manifest.getInternalName(), e);
+            eventBus.post(new ExternalPluginsChanged());
         }
     });
 }

If you’d prefer explicit user feedback, emit a dedicated event (e.g., PluginInstallationFailed(manifest, reason)), and have the UI react accordingly. I can draft that event and wire-up handling if desired.

🧹 Nitpick comments (5)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (2)

589-606: Incorrect error message and double-render; bail early on counts failure

This catch handles plugin-count download, but the label says “manifest failed”. Also, the code proceeds to reloadPluginList(manifestCollection, pluginCounts) after scheduling the error UI, which immediately replaces the error UI, making “Retry” pointless.

Apply:

             catch (IOException e)
             {
                 log.warn("Unable to download plugin counts", e);
                 SwingUtilities.invokeLater(() ->
                 {
                     refreshing.setVisible(false);
-                    mainPanel.add(new JLabel("Downloading the plugin manifest failed"));
+                    mainPanel.add(new JLabel("Unable to download plugin counts"));
 
                     JButton retry = new JButton("Retry");
                     retry.addActionListener(l -> reloadPluginList());
                     mainPanel.add(retry);
                     mainPanel.revalidate();
                 });
+                return; // don't proceed to reload with empty counts; keep the retry UI visible
             }

612-672: Minor: avoid magic numbers for icon scaling and add null/blank iconUrl guard

The 48x70 constants in PluginIcon.load() are duplicated knowledge of PluginItem dimensions, and iconUrl can be null/blank in some manifests.

  • Consider hoisting ICON_WIDTH/HEIGHT to outer class constants (or reference PluginItem.ICON_WIDTH/HEIGHT if made package-visible) to avoid drift.
  • Add a null/blank check before enqueueing/downloading.

Example (outside current diff):

// In PluginIcon
private boolean hasIcon() { return iconUrl != null && !iconUrl.isBlank(); }

@Override
public void paint(Graphics g) {
  super.paint(g);
  if (!loadingStarted && hasIcon()) {
    loadingStarted = true;
    synchronized (iconLoadQueue) {
      iconLoadQueue.add(this);
      if (iconLoadQueue.size() == 1) {
        executor.submit(MicrobotPluginHubPanel.this::pumpIconQueue);
      }
    }
  }
}

private void load() {
  try {
    if (!hasIcon()) { return; }
    BufferedImage img = microbotPluginClient.downloadIcon(iconUrl);
    if (img == null) { return; }
    BufferedImage scaledImg = scaleImageToFit(img, ICON_WIDTH, HEIGHT);
    SwingUtilities.invokeLater(() -> setIcon(new ImageIcon(scaledImg)));
  } catch (IOException e) {
    log.info("Cannot download icon \"{}\"", iconUrl, e);
  }
}
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (3)

332-341: verifyHash throws on bad inputs; prefer soft-fail to avoid executor-level exceptions

Throwing IllegalArgumentException here bubbles up unless every caller catches it. Given multiple call sites, returning false on invalid inputs keeps the error handling uniform and avoids noisy executor exceptions.

Apply:

-    if ((expectedHash == null || expectedHash.isEmpty()) || (jarData == null || jarData.length == 0))
-    {
-        throw new IllegalArgumentException("Hash or jar data is null/empty");
-    }
+    if ((expectedHash == null || expectedHash.isEmpty()) || (jarData == null || jarData.length == 0))
+    {
+        log.error("Hash or jar data is null/empty");
+        return false;
+    }
 
-    String computedHash = calculateSHA256Hash(jarData);
-    return computedHash.equals(expectedHash);
+    String computedHash = calculateSHA256Hash(jarData);
+    return computedHash.equalsIgnoreCase(expectedHash.trim());

100-115: Manifest-change detection ignores non-hash metadata updates

The “changed” condition only considers keyset and SHA-256 changes. UI-visible fields (displayName, description, tags, iconUrl) won’t trigger refresh if hashes remain the same.

Consider including additional manifest fields in the equality check (or compare entire manifest sans transient fields) so metadata updates refresh the panel. Minimal approach:

  • Compare version, displayName, description, and iconUrl in addition to sha256.

251-260: Optional: write jar atomically to reduce partial-write risk

If the process dies between Files.write() and saveInstalledPlugins(), a partially written jar may be left behind.

Write to a temp file then Files.move(temp, pluginFile) with ATOMIC_MOVE if supported, and only then update plugins.json.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f0a8704 and a127679.

📒 Files selected for processing (2)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (6 hunks)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-23T16:17:37.571Z
Learnt from: g-mason0
PR: chsami/Microbot#1423
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java:366-389
Timestamp: 2025-08-23T16:17:37.571Z
Learning: The ScheduledExecutorService injected into MicrobotPluginManager (and other RuneLite classes) is configured as a single-threaded executor via Executors.newSingleThreadScheduledExecutor() at RuneLiteModule.java line 130, wrapped in ExecutorServiceExceptionLogger. This ensures all async tasks execute sequentially, preventing race conditions.

Applied to files:

  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java
🧬 Code graph analysis (1)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java (1)
runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java (1)
  • RuneLiteProperties (35-162)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build

Comment on lines +485 to +489
catch (PluginInstantiationException | IOException e)
{
log.trace("Error loading side-loaded plugin!", e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Broaden catch to cover hash/argument issues during sideload load

verifyHash can throw IllegalArgumentException (until refactored). Catch it here to keep the executor log clean and ensure we don’t silently stop processing other plugins.

Apply:

-        catch (PluginInstantiationException | IOException e)
+        catch (PluginInstantiationException | IOException | IllegalArgumentException e)
         {
             log.trace("Error loading side-loaded plugin!", e);
         }
🤖 Prompt for AI Agents
In
runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java
around lines 485 to 489, the current catch only handles
PluginInstantiationException and IOException but verifyHash may throw
IllegalArgumentException; broaden the catch clause to include
IllegalArgumentException (or add a separate catch for it) so that these errors
are logged with log.trace("Error loading side-loaded plugin!", e) and do not
abort processing of other plugins.

@gmason0 gmason0 merged commit b53afd5 into chsami:development Aug 23, 2025
2 checks passed
This was referenced Sep 20, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 5, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 27, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jan 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant