Skip to content

Instantly share code, notes, and snippets.

@abextm
Last active November 20, 2021 18:01
Show Gist options
  • Save abextm/406be37cc74edddb0157dfce8bec372b to your computer and use it in GitHub Desktop.
Save abextm/406be37cc74edddb0157dfce8bec372b to your computer and use it in GitHub Desktop.
janky hmr
package abex.os.hmr;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import javax.inject.Inject;
import javax.swing.SwingUtilities;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.RuneLite;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.events.ExternalPluginsChanged;
import net.runelite.client.externalplugins.ExternalPluginManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.PluginInstantiationException;
import net.runelite.client.plugins.PluginManager;
import net.runelite.client.util.ReflectUtil;
@Slf4j
public class ExternalPluginHotReloader
{
private static List<String> pluginClassNames;
public ExternalPluginHotReloader() throws IOException
{
}
public static void main(String... args) throws Exception
{
File pluginFile = new File("runelite-plugin.properties");
if (!pluginFile.exists())
{
throw new RuntimeException("runelite-plugin.properties does not exist. Perhaps your working directory is incorrect. It should be the root of your plugin hub plugin.");
}
Properties props = loadProperties(pluginFile);
//TODO: validate the whole thing
{
String pluginsStr = (String) props.remove("plugins");
if (pluginsStr == null)
{
throw new RuntimeException("plugins must be set in runelite-plugin.properties");
}
pluginClassNames = Splitter.on(CharMatcher.anyOf(",:;"))
.omitEmptyStrings()
.trimResults()
.splitToList(pluginsStr);
}
ExternalPluginManager.loadBuiltin(HMRPlugin.class);
RuneLite.main(args);
}
@PluginDescriptor(
name = "hmr",
description = "",
hidden = true)
public static class HMRPlugin extends Plugin
{
@Inject
private ExternalPluginHotReloader hmr;
@Override
protected void startUp() throws Exception
{
hmr.startLater(0);
}
}
private static Properties loadProperties(File path) throws IOException
{
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(path))
{
props.load(fis);
}
return props;
}
private class ProxyCL extends ClassLoader implements ReflectUtil.PrivateLookupableClassLoader
{
@Getter
@Setter
private MethodHandles.Lookup lookup;
private final ClassLoader realParent;
public ProxyCL()
{
super(null);
this.realParent = getClass().getClassLoader();
ReflectUtil.installLookupHelper(this);
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
URL resource = realParent.getResource(name.replace('.', '/') + ".class");
if (resource != null && resource.toString().endsWith(".class"))
{
try
{
File f = new File(resource.toURI());
byte[] data = Files.toByteArray(f);
watch(f);
return defineClass(name, data, 0, data.length);
}
catch (IOException e)
{
log.warn("{}", resource, e);
}
catch (URISyntaxException | IllegalArgumentException ignored)
{
}
}
return realParent.loadClass(name);
}
@Override
public Class<?> defineClass0(String name, byte[] b, int off, int len) throws ClassFormatError
{
return super.defineClass(name, b, off, len);
}
}
private final Set<File> filesOfInterest = new HashSet<>();
private final WatchService watcher = FileSystems.getDefault().newWatchService();
@Inject
private PluginManager pluginManager;
@Inject
private EventBus eventBus;
private final List<Plugin> loaded = new ArrayList<>();
private final Timer timer = new Timer(true);
private TimerTask task;
@SneakyThrows
private void watch(File f)
{
if (!filesOfInterest.add(f))
{
return;
}
if (!f.isDirectory())
{
f = f.getParentFile();
if (!filesOfInterest.add(f))
{
return;
}
}
f.toPath().register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
}
{
Thread t = new Thread(() ->
{
for (; ; )
{
try
{
WatchKey key = watcher.take();
key.pollEvents();
startLater(500);
key.reset();
}
catch (Exception e)
{
log.warn("", e);
}
}
}, "HMR");
t.setDaemon(true);
t.start();
}
private synchronized void startLater(int delayMS)
{
if (task != null)
{
task.cancel();
}
task = new TimerTask()
{
@Override
public void run()
{
start();
}
};
timer.schedule(task, delayMS);
}
private void start()
{
log.info("Reloading");
for (Plugin p : loaded)
{
try
{
SwingUtilities.invokeAndWait(() ->
{
try
{
pluginManager.stopPlugin(p);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
});
}
catch (InterruptedException | InvocationTargetException e)
{
log.warn("Unable to stop external plugin \"{}\"", p.getClass().getName(), e);
}
pluginManager.remove(p);
}
loaded.clear();
ProxyCL pcl = new ProxyCL();
for (String className : pluginClassNames)
{
try
{
List<Class<?>> clazzes = new ArrayList<>();
clazzes.add(pcl.loadClass(className));
List<Plugin> newPlugins2 = pluginManager.loadPlugins(clazzes, null);
loaded.addAll(newPlugins2);
pluginManager.loadDefaultPluginConfiguration(newPlugins2);
SwingUtilities.invokeAndWait(() ->
{
try
{
for (Plugin p : newPlugins2)
{
pluginManager.startPlugin(p);
}
}
catch (PluginInstantiationException e)
{
throw new RuntimeException(e);
}
});
}
catch (ThreadDeath e)
{
throw e;
}
catch (Throwable e)
{
log.warn("Unable to start or load external plugin \"{}\"", className, e);
}
}
eventBus.post(new ExternalPluginsChanged(new ArrayList<>()));
log.info("Done");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment