Merge pull request #2611 from swazrgb/externalplugins-developmentmode

This commit is contained in:
Owain van Brakel
2020-05-24 21:09:50 +02:00
committed by GitHub
3 changed files with 234 additions and 101 deletions

View File

@@ -24,6 +24,7 @@
*/ */
package net.runelite.client; package net.runelite.client;
import com.google.common.base.Strings;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Properties; import java.util.Properties;
@@ -43,6 +44,7 @@ public class RuneLiteProperties
private static final String PATREON_LINK = "runelite.patreon.link"; private static final String PATREON_LINK = "runelite.patreon.link";
private static final String LAUNCHER_VERSION_PROPERTY = "launcher.version"; private static final String LAUNCHER_VERSION_PROPERTY = "launcher.version";
private static final String PLUGIN_PATH = "plugin.path"; private static final String PLUGIN_PATH = "plugin.path";
private static final String PLUGIN_DEVELOPMENT_PATH = "plugin.development.path";
private static final String TROUBLESHOOTING_LINK = "runelite.wiki.troubleshooting.link"; private static final String TROUBLESHOOTING_LINK = "runelite.wiki.troubleshooting.link";
private static final String BUILDING_LINK = "runelite.wiki.building.link"; private static final String BUILDING_LINK = "runelite.wiki.building.link";
private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; private static final String DNS_CHANGE_LINK = "runelite.dnschange.link";
@@ -146,6 +148,20 @@ public class RuneLiteProperties
return pluginPath.equals("") ? null : pluginPath; return pluginPath.equals("") ? null : pluginPath;
} }
public static String[] getPluginDevelopmentPath()
{
// First check if property supplied as environment variable PLUGIN_DEVELOPMENT_PATHS
String developmentPluginPaths = System.getenv(PLUGIN_DEVELOPMENT_PATH.replace('.', '_').toUpperCase());
if (Strings.isNullOrEmpty(developmentPluginPaths))
{
// Otherwise check the property file
developmentPluginPaths = properties.getProperty(PLUGIN_DEVELOPMENT_PATH);
}
return Strings.isNullOrEmpty(developmentPluginPaths) ? new String[0] : developmentPluginPaths.split(";");
}
public static String getImgurClientId() public static String getImgurClientId()
{ {
return properties.getProperty(IMGUR_CLIENT_ID); return properties.getProperty(IMGUR_CLIENT_ID);

View File

@@ -32,6 +32,7 @@ import com.google.inject.Key;
import com.google.inject.Module; import com.google.inject.Module;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.FileFilter;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@@ -40,6 +41,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -80,8 +82,13 @@ import net.runelite.client.util.Groups;
import net.runelite.client.util.MiscUtils; import net.runelite.client.util.MiscUtils;
import net.runelite.client.util.SwingUtil; import net.runelite.client.util.SwingUtil;
import org.jgroups.Message; import org.jgroups.Message;
import org.pf4j.BasePluginLoader;
import org.pf4j.CompoundPluginLoader;
import org.pf4j.CompoundPluginRepository;
import org.pf4j.DefaultPluginManager; import org.pf4j.DefaultPluginManager;
import org.pf4j.DependencyResolver; import org.pf4j.DependencyResolver;
import org.pf4j.DevelopmentPluginClasspath;
import org.pf4j.DevelopmentPluginRepository;
import org.pf4j.JarPluginLoader; import org.pf4j.JarPluginLoader;
import org.pf4j.JarPluginRepository; import org.pf4j.JarPluginRepository;
import org.pf4j.ManifestPluginDescriptorFinder; import org.pf4j.ManifestPluginDescriptorFinder;
@@ -114,6 +121,8 @@ public class ExternalPluginManager
private final EventBus eventBus; private final EventBus eventBus;
private final ConfigManager configManager; private final ConfigManager configManager;
private final Map<String, String> pluginsMap = new HashMap<>(); private final Map<String, String> pluginsMap = new HashMap<>();
@Getter
private final boolean developmentMode = RuneLiteProperties.getPluginDevelopmentPath().length > 0;
@Getter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC)
private final Map<String, Map<String, String>> pluginsInfoMap = new HashMap<>(); private final Map<String, Map<String, String>> pluginsInfoMap = new HashMap<>();
private final Groups groups; private final Groups groups;
@@ -153,45 +162,182 @@ public class ExternalPluginManager
@Override @Override
protected PluginDescriptorFinder createPluginDescriptorFinder() protected PluginDescriptorFinder createPluginDescriptorFinder()
{ {
return new ManifestPluginDescriptorFinder(); return new ManifestPluginDescriptorFinder()
}
@Override
protected PluginRepository createPluginRepository()
{
return new JarPluginRepository(getPluginsRoot())
{ {
@Override protected Path getManifestPath(Path pluginPath)
public List<Path> getPluginPaths()
{ {
File[] files = pluginsRoot.toFile().listFiles(filter); if (isDevelopment())
if ((files == null) || files.length == 0)
{ {
return Collections.emptyList(); // The superclass performs a find, which is slow in development mode since we're pointing
// at a sources directory, which can have a lot of files. The external plugin template
// will always output the manifest at the following location, so we can hardcode this path.
return pluginPath.resolve("build/tmp/jar/MANIFEST.MF");
} }
List<Path> paths = new ArrayList<>(files.length); return super.getManifestPath(pluginPath);
for (File file : files)
{
paths.add(file.toPath());
}
return paths;
} }
}; };
} }
@Override @Override
protected PluginLoader createPluginLoader() protected PluginRepository createPluginRepository()
{ {
return new JarPluginLoader(this); CompoundPluginRepository compoundPluginRepository = new CompoundPluginRepository();
if (isNotDevelopment())
{
JarPluginRepository jarPluginRepository = new JarPluginRepository(getPluginsRoot())
{
@Override
public List<Path> getPluginPaths()
{
File[] files = pluginsRoot.toFile().listFiles(filter);
if ((files == null) || files.length == 0)
{
return Collections.emptyList();
}
List<Path> paths = new ArrayList<>(files.length);
for (File file : files)
{
paths.add(file.toPath());
}
return paths;
}
};
compoundPluginRepository.add(jarPluginRepository);
}
if (isDevelopment())
{
FileFilter pluginsFilter = new FileFilter()
{
private final List<String> blacklist = Arrays.asList(
".git",
"build",
"target"
);
private final List<String> buildFiles = Arrays.asList(
"%s.gradle.kts",
"%s.gradle"
);
@Override
public boolean accept(File pathName)
{
// Check if this path looks like a plugin development directory
if (!pathName.isDirectory())
{
return false;
}
String dirName = pathName.getName();
if (blacklist.contains(dirName))
{
return false;
}
boolean isPlugin = false;
// By convention plugins their directory is $name and they have a $name.gradle.kts or $name.gradle file in their root
for (String buildFile : buildFiles)
{
if (new File(pathName, String.format(buildFile, dirName)).exists())
{
isPlugin = true;
break;
}
}
// It is a plugin directory, but we should also check if it can actually be loaded
if (!new File(pathName, "build/tmp/jar/MANIFEST.MF").exists())
{
return false;
}
return isPlugin;
}
};
for (String developmentPluginPath : RuneLiteProperties.getPluginDevelopmentPath())
{
DevelopmentPluginRepository developmentPluginRepository = new DevelopmentPluginRepository(Paths.get(developmentPluginPath))
{
@Override
public boolean deletePluginPath(Path pluginPath)
{
// Do nothing, because we'd be deleting our sources!
return filter.accept(pluginPath.toFile());
}
};
developmentPluginRepository.setFilter(pluginsFilter);
compoundPluginRepository.add(developmentPluginRepository);
}
}
return compoundPluginRepository;
} }
@Override @Override
public RuntimeMode getRuntimeMode() protected PluginLoader createPluginLoader()
{ {
return RuneLiteProperties.getLauncherVersion() == null ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; return new CompoundPluginLoader()
.add(new BasePluginLoader(this, new DevelopmentPluginClasspath().addJarsDirectories("build/deps")), this::isDevelopment)
.add(new JarPluginLoader(this), this::isNotDevelopment);
}
@Override
public void loadPlugins()
{
if (Files.notExists(pluginsRoot) || !Files.isDirectory(pluginsRoot))
{
log.warn("No '{}' root", pluginsRoot);
return;
}
List<Path> pluginPaths = pluginRepository.getPluginPaths();
if (pluginPaths.isEmpty())
{
log.warn("No plugins");
return;
}
log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths);
for (Path pluginPath : pluginPaths)
{
try
{
loadPluginFromPath(pluginPath);
}
catch (PluginRuntimeException e)
{
if (!(e instanceof PluginAlreadyLoadedException))
{
log.error("Could not load plugin {}", pluginPath, e);
}
}
}
try
{
resolvePlugins();
}
catch (PluginRuntimeException e)
{
if (e instanceof DependencyResolver.DependenciesNotFoundException)
{
throw e;
}
log.error(e.getMessage(), e);
}
} }
@Override @Override
@@ -258,52 +404,9 @@ public class ExternalPluginManager
} }
@Override @Override
public void loadPlugins() public RuntimeMode getRuntimeMode()
{ {
if (Files.notExists(pluginsRoot) || !Files.isDirectory(pluginsRoot)) return developmentMode ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT;
{
log.warn("No '{}' root", pluginsRoot);
return;
}
List<Path> pluginPaths = pluginRepository.getPluginPaths();
if (pluginPaths.isEmpty())
{
log.warn("No plugins");
return;
}
log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths);
for (Path pluginPath : pluginPaths)
{
try
{
loadPluginFromPath(pluginPath);
}
catch (PluginRuntimeException e)
{
if (!(e instanceof PluginAlreadyLoadedException))
{
log.error(e.getMessage(), e);
}
}
}
try
{
resolvePlugins();
}
catch (PluginRuntimeException e)
{
if (e instanceof DependencyResolver.DependenciesNotFoundException)
{
throw e;
}
log.error(e.getMessage(), e);
}
} }
@Override @Override
@@ -1038,30 +1141,39 @@ public class ExternalPluginManager
return true; return true;
} }
// Null version returns the last release version of this plugin for given system version
try try
{ {
PluginInfo.PluginRelease latest = updateManager.getLastPluginRelease(pluginId); if (!developmentMode)
if (latest == null)
{ {
try PluginInfo.PluginRelease latest = updateManager.getLastPluginRelease(pluginId);
// Null version returns the last release version of this plugin for given system version
if (latest == null)
{ {
SwingUtil.syncExec(() -> try
JOptionPane.showMessageDialog(ClientUI.getFrame(), {
pluginId + " is outdated and cannot be installed", SwingUtil.syncExec(() ->
"Installation error", JOptionPane.showMessageDialog(ClientUI.getFrame(),
JOptionPane.ERROR_MESSAGE)); pluginId + " is outdated and cannot be installed",
} "Installation error",
catch (InvocationTargetException | InterruptedException ignored) JOptionPane.ERROR_MESSAGE));
{ }
return false; catch (InvocationTargetException | InterruptedException ignored)
{
return false;
}
return true;
} }
return true; updateManager.installPlugin(pluginId, null);
}
else
{
// In development mode our plugin will already be present in a repository, so we can just load it
externalPluginManager.loadPlugins();
externalPluginManager.startPlugin(pluginId);
} }
updateManager.installPlugin(pluginId, null);
scanAndInstantiate(loadPlugin(pluginId), true, true); scanAndInstantiate(loadPlugin(pluginId), true, true);

View File

@@ -147,32 +147,37 @@ public class PluginListItem extends JPanel
String pluginId = pluginInfo.get("id"); String pluginId = pluginInfo.get("id");
hotSwapButton.setIcon(REFRESH_ICON); hotSwapButton.setIcon(REFRESH_ICON);
externalPluginManager.uninstall(pluginId);
SwingWorker<Boolean, Void> worker = new SwingWorker<>() new SwingWorker<>()
{ {
@Override @Override
protected Boolean doInBackground() protected Boolean doInBackground()
{ {
return externalPluginManager.uninstall(pluginId); return externalPluginManager.uninstall(pluginId);
} }
};
worker.execute();
JOptionPane.showMessageDialog(ClientUI.getFrame(),
pluginId + " is unloaded, put the new jar file in the externalmanager folder and click `ok`",
"Hotswap " + pluginId,
JOptionPane.INFORMATION_MESSAGE);
worker = new SwingWorker<>()
{
@Override @Override
protected Boolean doInBackground() protected void done()
{ {
return externalPluginManager.reloadStart(pluginId); // In development mode our plugins will be loaded directly from sources, so we don't need to prompt
if (!externalPluginManager.isDevelopmentMode())
{
JOptionPane.showMessageDialog(ClientUI.getFrame(),
pluginId + " is unloaded, put the new jar file in the externalmanager folder and click `ok`",
"Hotswap " + pluginId,
JOptionPane.INFORMATION_MESSAGE);
}
new SwingWorker<>()
{
@Override
protected Boolean doInBackground()
{
return externalPluginManager.reloadStart(pluginId);
}
}.execute();
} }
}; }.execute();
worker.execute();
}); });
hotSwapButton.setVisible(true); hotSwapButton.setVisible(true);